diff --git a/doc/requirements.txt b/doc/requirements.txt new file mode 100644 index 000000000..f4aece6ac --- /dev/null +++ b/doc/requirements.txt @@ -0,0 +1,4 @@ +bokeh +netCDF4 +numpy +scipy diff --git a/doc/source/index.rst b/doc/source/index.rst index 25e9a4a0a..4b4a2581f 100644 --- a/doc/source/index.rst +++ b/doc/source/index.rst @@ -3,10 +3,10 @@ You can adapt this file completely to your liking, but it should at least contain the root `toctree` directive. -Forest - forecast evaluation tools +FOREST - Forecast evaluation tools ================================== -Forest is a framework for evaluating geophysical numerical models. Forecast +FOREST is a framework for evaluating geophysical numerical models. Forecast and nowcast fields can be compared to equivalent observations using a simple user interface. @@ -16,6 +16,7 @@ The code is available on `GitHub `_ a :maxdepth: 2 :caption: Contents: + start user guide api diff --git a/doc/source/start.rst b/doc/source/start.rst new file mode 100644 index 000000000..d450fcaa0 --- /dev/null +++ b/doc/source/start.rst @@ -0,0 +1,159 @@ + +Getting started +=============== + +Welcome to the complete guide to FOREST. Learn how +to integrate FOREST into your existing work flow, build a +web portal or quickly view your model diagnostics alongside +observations. + +Installation +------------ + +FOREST is distributed via conda through the `conda-forge` channel + +.. code-block:: sh + + :> conda install -c conda-forge forest -y + +Full documentation for conda can be found here: https://docs.conda.io/en/latest/ + +Who is FOREST for? +~~~~~~~~~~~~~~~~~~ + +FOREST is intended to provide a step change in exploration and +monitoring of forecasting systems. Technical and non-technical +users should be able to easily compare, interrogate and report on the +quality of forecasts. + +While the primary intention of FOREST is to support research-mode activities +it should be trivial to use in an operational context. The underlying +technologies scale seemlessly from a single user running on a laptop +up to a fleet of EC2 instances running on AWS. + +Tutorial +-------- + +FOREST comes with example cases intended to get users off the ground +quickly, reading about a tool is all well and good but nothing compares +to hands on experience. + +.. code-block:: bash + + ~: forest-tutorial -h + +The only argument `forest-tutorial` needs is a directory to place +files. Go ahead and run the tutorial command to +get your hands on some files that `forest` can analyse. + +.. code-block:: bash + + ~: forest-tutorial . + +The above snippet can be used to populate the current working directory with +all of the inputs needed to run the `forest` command line interface + +Example - Unified model output +------------------------------ + +To display the unified model without any additional configuration simply +run the following command inside a shell prompt + +.. code-block:: bash + + ~: forest --show unified_model.nc + + +Example - Rapidly developing thunderstorms +------------------------------------------ + +The above example shows how `forest` can be used in a similar mode to well-known +utilities, e.g. `xconv`, `ncview` etc. However, given we have a full Tornado +server running and the power of Python at our finger tips it would be +criminal to curtail our application. To go beyond vanilla `ncview` behaviour +try the following command: + +.. code-block:: bash + + ~: forest --show --file-type rdt rdt_*.json + +This should bring up a novel polygon geojson visualisation of satellite +RDT (rapidly developing thunderstorms). But wait, without the underlying +OLR (outgoing longwave radiation) layer the polygons by themselves are +of little value + +.. code-block:: bash + + ~: forest --show --file-type eida50 eida50*.nc + +It seems we are beginning to outgrow the command line, wouldn't it be +nice if we could store our settings and use them in a reproducible way! + +Example - Multiple data sources +------------------------------- + +Open up `config.yml` for an example of the settings that can be adjusted +to suit your particular use case. + +.. code-block:: yaml + + files: + - label: UM + pattern: unified_model*.nc + locator: file_system + - label: RDT + pattern: rdt*.json + locator: file_system + - label: EIDA50 + pattern: eida50*.nc + locator: file_system + +Running the following command should load FOREST with a model diagnostic, +satellite image and derived polygon product at the same time that can be +simultaneously compared + +Example - Going faster with SQL +------------------------------- + +For very large data sets file access and meta-data checking +becomes a bottle neck. Accessing thousands or even hundreds of files +to answer a single query can be time consuming, especially if your +files are stored in the cloud, e.g. in an S3 bucket. A simple way to address +this issue is to harvest the meta-data once and then use the power +of a query language and relational database to quickly lookup +files and indices. + +.. code-block:: sh + + :> forest --show --config-file config.yml --database database.db + +To generate a database from scratch use the `forestdb` command. + +.. code-block:: sh + + :> forestdb --database my-database.db my-file-*.nc + +.. note:: To switch on database-powered menu systems change `locator` to + `database` in the config file + +.. note:: Database support is only available for unified_model file types + +.. note:: Prefix pattern with wildcard `*` to enable SQL queries to find files + +.. code-block:: yaml + + files: + - label: UM + pattern: "*unified_model.nc" + locator: database + - label: RDT + pattern: rdt*.json + locator: file_system + - label: EIDA50 + pattern: eida50*.nc + locator: file_system + +With the updated config file and correctly populated database, the server running +forest should have less work to do to harvest meta-data at startup. This +performance boost makes forest more responsive when viewing large datasets +consisting of thousands of files. diff --git a/forest/__init__.py b/forest/__init__.py index d834c5a54..cd0c7bf10 100644 --- a/forest/__init__.py +++ b/forest/__init__.py @@ -6,3 +6,8 @@ __version__ = '0.3.0' from .config import * +from . import ( + navigate, + unified_model, + tutorial) +from .db import Database diff --git a/forest/app/__init__.py b/forest/app/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/forest/app/main.py b/forest/app/main.py deleted file mode 100644 index dc2c49652..000000000 --- a/forest/app/main.py +++ /dev/null @@ -1,53 +0,0 @@ -import bokeh.plotting -import argparse -import yaml -import sys -import os -sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..")) - -from .. import db - - -def parse_args(argv=None): - parser = argparse.ArgumentParser() - parser.add_argument( - "--database", - required=True, - help="SQL database to optimise menu system") - parser.add_argument( - "--config-file", - required=True, metavar="YAML_FILE", - help="YAML file to configure application") - return parser.parse_args(args=argv) - - -def main(): - args = parse_args() - with open(args.config_file) as stream: - config = load_config(stream) - database = db.Database.connect(args.database) - controls = db.Controls(database, patterns=config.patterns) - controls.subscribe(print) - - locator = db.Locator.connect(args.database) - text = db.View(text="Hello, world!", locator=locator) - controls.subscribe(text.on_state) - - document = bokeh.plotting.curdoc() - document.add_root(controls.layout) - document.add_root(text.div) - - -class Namespace(object): - def __init__(self, **kwargs): - self.__dict__.update(**kwargs) - - -def load_config(stream): - data = yaml.load(stream) - patterns = [(m["name"], m["pattern"]) for m in data["models"]] - return Namespace(patterns=patterns) - - -if __name__.startswith('bk'): - main() diff --git a/forest/cli/main.py b/forest/cli/main.py index 3ad46c134..b6e24cf3f 100644 --- a/forest/cli/main.py +++ b/forest/cli/main.py @@ -4,6 +4,7 @@ import os import argparse import bokeh.command.bootstrap +from forest.parse_args import add_arguments APP_PATH = os.path.join(os.path.dirname(__file__), "..") @@ -14,18 +15,7 @@ def parse_args(args=None): # FOREST specific arguments group = parser.add_argument_group('forest arguments') - group.add_argument( - "files", nargs="*", metavar="FILE", - help="zero or more files to display") - group.add_argument( - "--config", - help="file to configure forest in addition to file(s)") - group.add_argument( - "--database", - help="sql file to enhance navigation") - group.add_argument( - "--directory", - help="replace directory of database files") + add_arguments(group) # Bokeh serve pass-through arguments group = parser.add_argument_group('bokeh serve arguments') @@ -43,8 +33,8 @@ def parse_args(args=None): help="public hostnames that may connect to the websocket") args = parser.parse_args(args=args) - if len(args.files) == 0 and args.config is None: - parser.error("please specify file(s) or a valid --config file") + if len(args.files) == 0 and args.config_file is None: + parser.error("please specify file(s) or a valid --config-file file") return args @@ -65,12 +55,14 @@ def bokeh_args(app_path, args): if args.allow_websocket_origin: opts += ["--allow-websocket-origin", str(args.allow_websocket_origin)] extra = [] - if args.config is not None: - extra += ["--config-file", str(args.config)] + if args.config_file is not None: + extra += ["--config-file", str(args.config_file)] if args.database is not None: extra += ["--database", str(args.database)] if args.directory is not None: extra += ["--directory", str(args.directory)] + if args.file_type != "unified_model": + extra += ["--file-type", str(args.file_type)] if len(args.files) > 0: extra += args.files if len(extra) > 0: diff --git a/forest/config.py b/forest/config.py index 90cc747ae..91489f221 100644 --- a/forest/config.py +++ b/forest/config.py @@ -31,9 +31,19 @@ def patterns(self): @classmethod def load(cls, path): with open(path) as stream: - data = yaml.load(stream) + try: + # PyYaml 5.1 onwards + data = yaml.full_load(stream) + except AttributeError: + data = yaml.load(stream) return cls(data) + @classmethod + def from_files(cls, files, file_type="unified_model"): + return cls({ + "files": [dict(pattern=f, label=f, file_type=file_type) + for f in files]}) + @property def file_groups(self): return [FileGroup(**data) @@ -94,3 +104,8 @@ def _str(value): @export def load_config(path): return Config.load(path) + + +@export +def from_files(files, file_type): + return Config.from_files(files, file_type) diff --git a/forest/data.py b/forest/data.py index 445298790..81f04848c 100644 --- a/forest/data.py +++ b/forest/data.py @@ -1,25 +1,33 @@ import os import datetime as dt import re -import cartopy +try: + import cartopy +except ImportError: + # ReadTheDocs unable to pip install cartopy + pass import glob import json import pandas as pd import numpy as np import netCDF4 import cf_units -import forest.satellite as satellite -import forest.rdt as rdt -import forest.earth_networks as earth_networks -import forest.geo as geo +from forest import ( + satellite, + rdt, + earth_networks, + geo, + disk) import bokeh.models from collections import OrderedDict, defaultdict from functools import partial import scipy.ndimage import shapely.geometry -from forest.util import timeout_cache, initial_time, coarsify -from forest.db.exceptions import SearchFail -import forest.disk as disk +from forest.util import ( + timeout_cache, + initial_time, + coarsify) +from forest.exceptions import SearchFail # Application data shared across documents @@ -84,6 +92,8 @@ def file_loader(file_type, pattern): return earth_networks.Loader(pattern) elif file_type.lower() == 'eida50': return satellite.EIDA50(pattern) + else: + raise Exception("unrecognised file_type: {}".format(file_type)) def load_coastlines(): @@ -279,6 +289,20 @@ def __init__(self, name, pattern, locator): "level": [] } + @staticmethod + def to_datetime(d): + if isinstance(d, dt.datetime): + return d + elif isinstance(d, str): + try: + return dt.datetime.strptime(d, "%Y-%m-%d %H:%M:%S") + except ValueError: + return dt.datetime.strptime(d, "%Y-%m-%dT%H:%M:%S") + elif isinstance(d, np.datetime64): + return d.astype(dt.datetime) + else: + raise Exception("Unknown value: {}".format(d)) + def image(self, state): if not self.valid(state): return self.empty_image @@ -290,11 +314,12 @@ def image(self, state): state.initial_time, state.valid_time, state.pressure) + print("{}() {} {}".format(self.__class__.__name__, path, pts)) except SearchFail: return self.empty_image - valid = dt.datetime.strptime(state.valid_time, "%Y-%m-%d %H:%M:%S") - initial = dt.datetime.strptime(state.initial_time, "%Y-%m-%d %H:%M:%S") + valid = self.to_datetime(state.valid_time) + initial = self.to_datetime(state.initial_time) hours = (valid - initial).total_seconds() / (60*60) length = "T{:+}".format(int(hours)) data = load_image_pts( @@ -323,6 +348,8 @@ def valid(self, state): if state.pressures is None: return False if len(state.pressures) > 0: + if state.pressure is None: + return False if not self.has_pressure(state.pressures, state.pressure): return False return True @@ -427,10 +454,7 @@ def _load_netcdf4(self, path, variable, lon0, lat0, pressure=None): try: var = dataset.variables[variable] except KeyError: - return { - "x": [], - "y": [] - } + return [], [] lons = geo.to_180(self._longitudes(dataset, var)) lats = self._latitudes(dataset, var) i = np.argmin(np.abs(lons - lon0)) diff --git a/forest/db/__init__.py b/forest/db/__init__.py index c43c991e7..a1872a8f4 100644 --- a/forest/db/__init__.py +++ b/forest/db/__init__.py @@ -3,4 +3,3 @@ from .locate import * from .util import * from .view import * -from .exceptions import * diff --git a/forest/db/control.py b/forest/db/control.py index ee9fdd454..f90b5b88d 100644 --- a/forest/db/control.py +++ b/forest/db/control.py @@ -1,4 +1,8 @@ """Control navigation of FOREST data""" +import copy +import datetime as dt +import numpy as np +from functools import wraps import bokeh.models import bokeh.layouts from . import util @@ -7,12 +11,6 @@ __all__ = [ "State", - "ButtonClick", - "Message", - "Observable", - "Controls", - "next_state", - "initial_state" ] @@ -22,8 +20,29 @@ def export(obj): return obj +SET_VALUE = "SET_VALUE" +NEXT_VALUE = "NEXT_VALUE" +PREVIOUS_VALUE = "PREVIOUS_VALUE" + + +@export +def set_value(key, value): + return dict(kind=SET_VALUE, payload=locals()) + + +@export +def next_value(item_key, items_key): + return dict(kind=NEXT_VALUE, payload=locals()) + + +@export +def previous_value(item_key, items_key): + return dict(kind=PREVIOUS_VALUE, payload=locals()) + + State = namedtuple("State", ( "pattern", + "patterns", "variable", "variables", "initial_time", @@ -36,68 +55,7 @@ def export(obj): State.__new__.__defaults__ = (None,) * len(State._fields) -def next_state(current, **kwargs): - _kwargs = current._asdict() - _kwargs.update(kwargs) - return State(**_kwargs) - - -class Message(object): - def __init__(self, kind, payload): - self.kind = kind - self.payload = payload - - @classmethod - def button(cls, category, instruction): - return ButtonClick(category, instruction) - - @classmethod - def dropdown(cls, key, value): - return cls("dropdown", (key, value)) - - def __repr__(self): - if self.__class__.__module__ is not None: - names = (self.__class__.__module__, self.__class__.__name__) - else: - names = (self.__class__.__name__,) - - def stringify(value): - if isinstance(value, str): - return "'{}'".format(value) - else: - return str(value) - - args = (self.kind, self.payload) - return "{}({})".format( - ".".join(names), - ", ".join(map(stringify, args))) - - -class ButtonClick(object): - kind = "button" - - def __init__(self, category, instruction): - self.category = category - self.instruction = instruction - - def __repr__(self): - if self.__class__.__module__ is not None: - names = (self.__class__.__module__, self.__class__.__name__) - else: - names = (self.__class__.__name__,) - - def stringify(value): - if isinstance(value, str): - return "'{}'".format(value) - else: - return str(value) - - args = (self.category, self.instruction) - return "{}({})".format( - ".".join(names), - ", ".join(map(stringify, args))) - - +@export class Observable(object): def __init__(self): self.subscribers = [] @@ -109,130 +67,287 @@ def notify(self, state): for callback in self.subscribers: callback(state) +@export +class Stream(Observable): + def listen_to(self, observable): + observable.subscribe(self.notify) + return self + + def map(self, f): + stream = Stream() + def callback(x): + stream.notify(f(x)) + self.subscribe(callback) + return stream -def initial_state(database, pattern=None): - """Find initial state given database""" - variables = database.variables(pattern) - variable = first(variables) - initial_times = database.initial_times(pattern, variable) - initial_time = None - if len(initial_times) > 0: - initial_time = max(initial_times) - valid_times = database.valid_times( +@export +def initial_state(navigator, pattern=None): + """Find initial state given navigator""" + state = {} + state["pattern"] = pattern + variables = navigator.variables(pattern) + state["variables"] = variables + if len(variables) == 0: + return state + variable = variables[0] + state["variable"] = variable + initial_times = navigator.initial_times(pattern, variable) + state["initial_times"] = initial_times + if len(initial_times) == 0: + return state + initial_time = max(initial_times) + state["initial_time"] = initial_time + valid_times = navigator.valid_times( variable=variable, pattern=pattern, initial_time=initial_time) - valid_time = None + state["valid_times"] = valid_times if len(valid_times) > 0: - valid_time = min(valid_times) - pressures = database.pressures(variable, pattern, initial_time) + state["valid_time"] = min(valid_times) + pressures = navigator.pressures( + variable=variable, + pattern=pattern, + initial_time=initial_time) pressures = list(reversed(sorted(pressures))) - pressure = None + state["pressures"] = pressures if len(pressures) > 0: - pressure = pressures[0] - state = State( - pattern=pattern, - variable=variable, - variables=variables, - initial_time=initial_time, - initial_times=initial_times, - valid_time=valid_time, - valid_times=valid_times, - pressures=pressures, - pressure=pressure) + state["pressure"] = pressures[0] return state -def first(items): - for item in items: - return item +@export +def stamps(times): + labels = [] + for t in times: + if isinstance(t, np.datetime64): + t = t.astype(dt.datetime) + labels.append(str(t)) + return labels @export class Store(Observable): - def __init__(self, state=None): - if state is None: - state = State() - self.state = state + def __init__(self, reducer, initial_state=None, middlewares=None): self.reducer = reducer + self.state = initial_state if initial_state is not None else {} + if middlewares is not None: + mws = [m(self) for m in middlewares] + f = self.dispatch + for mw in reversed(mws): + f = mw(f) + self.dispatch = f + super().__init__() def dispatch(self, action): self.state = self.reducer(self.state, action) + self.notify(self.state) @export def reducer(state, action): - if action.kind == "button": - return button_reducer(state, action) + state = copy.copy(state) + kind = action["kind"] + if kind == SET_VALUE: + payload = action["payload"] + key, value = payload["key"], payload["value"] + state[key] = value return state -def button_reducer(state, action): - items, item = get_items(state, action) - if action.category == "pressure": - instruction = reverse(action.instruction) - else: - instruction = action.instruction - if instruction == "next": - item = next_item(items, item) - else: - item = previous_item(items, item) - return next_state(state, **{action.category: item}) +def middleware(f): + """Curries functions to satisfy middleware signature""" + @wraps(f) + def outer(*args): + def inner(next_dispatch): + def inner_most(action): + f(*args, next_dispatch, action) + return inner_most + return inner + return outer -def reverse(instruction): - if instruction == "next": - return "previous" - else: - return "next" +@export +class Log(object): + """Logs actions""" + def __init__(self, verbose=False): + self.verbose = verbose + self.actions = [] + @middleware + def __call__(self, store, next_dispatch, action): + value = next_dispatch(action) + if self.verbose: + print(action) + self.actions.append(action) + return value -def get_items(state, action): - if action.category == 'initial_time': - return state.initial_times, state.initial_time - elif action.category == 'valid_time': - return state.valid_times, state.valid_time - elif action.category == 'pressure': - return state.pressures, state.pressure - else: - raise Exception("Unrecognised category: {}".format(action.category)) + +@export +class InverseCoordinate(object): + """Translate actions on inverted coordinates""" + def __init__(self, name): + self.name = name + + @middleware + def __call__(self, store, next_dispatch, action): + kind = action["kind"] + if kind in [NEXT_VALUE, PREVIOUS_VALUE]: + if self.name == action["payload"]["item_key"]: + return next_dispatch(self.invert(action)) + return next_dispatch(action) + + @staticmethod + def invert(action): + kind = action["kind"] + payload = action["payload"] + item_key = payload["item_key"] + items_key = payload["items_key"] + if kind == NEXT_VALUE: + return previous_value(item_key, items_key) + else: + return next_value(item_key, items_key) + + +@export +@middleware +def next_previous(store, next_dispatch, action): + """Translate NEXT/PREVIOUS action(s) into SET action""" + kind = action["kind"] + if kind in [NEXT_VALUE, PREVIOUS_VALUE]: + payload = action["payload"] + item_key = payload["item_key"] + items_key = payload["items_key"] + if items_key not in store.state: + # No further action to be taken + return + items = store.state[items_key] + if item_key in store.state: + item = store.state[item_key] + if kind == NEXT_VALUE: + value = next_item(items, item) + else: + value = previous_item(items, item) + else: + if kind == NEXT_VALUE: + value = max(items) + else: + value = min(items) + return next_dispatch(set_value(item_key, value)) + return next_dispatch(action) def next_item(items, item): - if items is None: - return None - if item is None: - return max(items) items = list(sorted(items)) i = items.index(item) return items[(i + 1) % len(items)] def previous_item(items, item): - if items is None: - return None - if item is None: - return min(items) items = list(sorted(items)) i = items.index(item) return items[i - 1] -class Controls(Observable): - def __init__(self, database, patterns=None, state=None): - if patterns is None: - patterns = [] - self.patterns = patterns - self.database = database - if state is None: - state = State() - self.state = state +@export +class Navigator(object): + """Interface for navigation menu system""" + def variables(self, pattern): + return ['air_temperature'] + + def initial_times(self, pattern): + return ['2019-01-01 00:00:00'] + + def valid_times(self, pattern, variable, initial_time): + return ['2019-01-01 12:00:00'] + + def pressures(self, pattern, variable, initial_time): + return [750.] + + +@export +class Converter(object): + def __init__(self, maps): + self.maps = maps + + @middleware + def __call__(self, store, next_dispatch, action): + if action["kind"] == SET_VALUE: + key = action["payload"]["key"] + value = action["payload"]["value"] + if key in self.maps: + value = self.maps[key](value) + return next_dispatch(set_value(key, value)) + return next_dispatch(action) + + +@export +class Controls(object): + def __init__(self, navigator): + self.navigator = navigator + + @middleware + def __call__(self, store, next_dispatch, action): + if action["kind"] == SET_VALUE: + key = action["payload"]["key"] + value = action["payload"]["value"] + if (key == "pressure"): + try: + value = float(value) + except ValueError: + print("{} is not a float".format(value)) + return next_dispatch(set_value(key, value)) + elif key == "pattern": + variables = self.navigator.variables(pattern=value) + initial_times = self.navigator.initial_times(pattern=value) + initial_times = list(reversed(initial_times)) + next_dispatch(action) + next_dispatch(set_value("variables", variables)) + next_dispatch(set_value("initial_times", initial_times)) + return + elif key == "variable": + for attr in ["pattern", "initial_time"]: + if attr not in store.state: + return next_dispatch(action) + pattern = store.state["pattern"] + variable = value + initial_time = store.state["initial_time"] + valid_times = self.navigator.valid_times( + pattern=pattern, + variable=variable, + initial_time=initial_time) + valid_times = sorted(set(valid_times)) + pressures = self.navigator.pressures( + pattern=pattern, + variable=variable, + initial_time=initial_time) + pressures = list(reversed(pressures)) + next_dispatch(action) + next_dispatch(set_value("valid_times", valid_times)) + next_dispatch(set_value("pressures", pressures)) + return + elif key == "initial_time": + for attr in ["pattern", "variable"]: + if attr not in store.state: + return next_dispatch(action) + valid_times = self.navigator.valid_times( + pattern=store.state["pattern"], + variable=store.state["variable"], + initial_time=value) + valid_times = sorted(set(valid_times)) + next_dispatch(action) + next_dispatch(set_value("valid_times", valid_times)) + return + return next_dispatch(action) + + +@export +class ControlView(Observable): + def __init__(self): dropdown_width = 180 button_width = 75 self.dropdowns = { "pattern": bokeh.models.Dropdown( - label="Model/observation", - menu=patterns), + label="Model/observation"), "variable": bokeh.models.Dropdown( label="Variable"), "initial_time": bokeh.models.Dropdown( @@ -251,7 +366,10 @@ def __init__(self, database, patterns=None, state=None): dropdown.on_change("value", self.on_change(key)) self.rows = {} self.buttons = {} - for key in ['pressure', 'valid_time', 'initial_time']: + for key, items_key in [ + ('pressure', 'pressures'), + ('valid_time', 'valid_times'), + ('initial_time', 'initial_times')]: self.buttons[key] = { 'next': bokeh.models.Button( label="Next", @@ -260,35 +378,56 @@ def __init__(self, database, patterns=None, state=None): label="Previous", width=button_width), } - self.buttons[key]['next'].on_click(self.on_click(key, 'next')) - self.buttons[key]['previous'].on_click(self.on_click(key, 'previous')) + self.buttons[key]['next'].on_click( + self.on_next(key, items_key)) + self.buttons[key]['previous'].on_click( + self.on_previous(key, items_key)) self.rows[key] = bokeh.layouts.row( self.buttons[key]["previous"], self.dropdowns[key], self.buttons[key]["next"]) - self.layout = bokeh.layouts.column( self.dropdowns["pattern"], self.dropdowns["variable"], self.rows["initial_time"], self.rows["valid_time"], self.rows["pressure"]) - super().__init__() + def on_change(self, key): + def callback(attr, old, new): + self.notify(set_value(key, new)) + return callback + + def on_next(self, item_key, items_key): + def callback(): + self.notify(next_value(item_key, items_key)) + return callback + + def on_previous(self, item_key, items_key): + def callback(): + self.notify(previous_value(item_key, items_key)) + return callback + def render(self, state): """Configure dropdown menus""" - for key, values in [ - ("variable", state.variables), - ("initial_time", state.initial_times), - ("valid_time", state.valid_times), - ("pressure", state.pressures)]: - if values is None: + assert isinstance(state, dict), "Only support dict" + print(state) + for key, items_key in [ + ("pattern", "patterns"), + ("variable", "variables"), + ("initial_time", "initial_times"), + ("valid_time", "valid_times"), + ("pressure", "pressures")]: + if items_key not in state: disabled = True else: + values = state[items_key] disabled = len(values) == 0 if key == "pressure": - menu = [(self.hpa(p), str(p)) for p in state.pressures] + menu = [(self.hpa(p), str(p)) for p in values] + elif key == "pattern": + menu = state["patterns"] else: menu = self.menu(values) self.dropdowns[key].menu = menu @@ -297,74 +436,20 @@ def render(self, state): self.buttons[key]["next"].disabled = disabled self.buttons[key]["previous"].disabled = disabled - if state.pattern is not None: - self.dropdowns['pattern'].value = state.pattern - - if state.variable is not None: - self.dropdowns['variable'].value = state.variable - - if state.initial_time is not None: - self.dropdowns['initial_time'].value = state.initial_time - - if state.pressure is not None: - self.dropdowns["pressure"].value = str(state.pressure) - - if state.valid_time is not None: - self.dropdowns['valid_time'].value = state.valid_time - - def on_change(self, key): - """Wire up bokeh on_change callbacks to State changes""" - def callback(attr, old, new): - self.send(Message("dropdown", (key, new))) - return callback - - def on_click(self, category, instruction): - def callback(): - self.send(ButtonClick(category, instruction)) - return callback - - def send(self, message): - state = self.modify(self.state, message) - if state is not None: - self.notify(state) - self.state = state - - def modify(self, state, message): - """Adjust state given message""" - print(message) - if message.kind == 'dropdown': - key, value = message.payload - if (key == 'pressure') and (value is not None): - value = float(value) - state = next_state(state, **{key: value}) - if key != 'pressure': - if state.pattern is not None: - variables = self.database.variables(pattern=state.pattern) - state = next_state(state, variables=variables) - - initial_times = list(reversed( - self.database.initial_times(pattern=state.pattern))) - state = next_state(state, initial_times=initial_times) - - if state.initial_time is not None: - pressures = self.database.pressures( - pattern=state.pattern, - variable=state.variable, - initial_time=state.initial_time) - pressures = list(reversed(pressures)) - state = next_state(state, pressures=pressures) - - if state.initial_time is not None: - valid_times = self.database.valid_times( - pattern=state.pattern, - variable=state.variable, - initial_time=state.initial_time) - valid_times = sorted(set(valid_times)) - state = next_state(state, valid_times=valid_times) - - if message.kind == 'button': - state = button_reducer(state, message) - return state + if ("pattern" in state) and ("patterns" in state): + for _, pattern in state["patterns"]: + if pattern == state["pattern"]: + self.dropdowns["pattern"].value = pattern + + for key in [ + "variable", + "initial_time", + "pressure", + "valid_time"]: + if key in state: + if state[key] is None: + continue + self.dropdowns[key].value = str(state[key]) @staticmethod def menu(values, formatter=str): diff --git a/forest/db/database.py b/forest/db/database.py index ec9d871f5..2f85b5e97 100644 --- a/forest/db/database.py +++ b/forest/db/database.py @@ -1,4 +1,8 @@ -import iris +try: + import iris +except ImportError: + # ReadTheDocs can't install iris + pass import netCDF4 import jinja2 from .connection import Connection diff --git a/forest/db/exceptions.py b/forest/db/exceptions.py deleted file mode 100644 index 7bc038600..000000000 --- a/forest/db/exceptions.py +++ /dev/null @@ -1,4 +0,0 @@ - - -class SearchFail(Exception): - pass diff --git a/forest/db/locate.py b/forest/db/locate.py index c302503cb..717fcbf64 100644 --- a/forest/db/locate.py +++ b/forest/db/locate.py @@ -2,7 +2,7 @@ from functools import lru_cache import numpy as np from .connection import Connection -from .exceptions import SearchFail +from forest.exceptions import SearchFail __all__ = [ diff --git a/forest/disk.py b/forest/disk.py index c3475c3fe..172513c36 100644 --- a/forest/disk.py +++ b/forest/disk.py @@ -2,207 +2,103 @@ import netCDF4 import datetime as dt import numpy as np -import forest.util as util -class NoData(Exception): +class AxisNotFound(Exception): pass -def scrape(path): - """All meta-data in single transaction""" - scheme = { - 'dimensions': [], - 'variables': {} - } - with netCDF4.Dataset(path) as dataset: - for d in dataset.dimensions: - scheme['dimensions'].append(d) - for v, obj in dataset.variables.items(): - scheme['variables'][v] = { - 'dimensions': obj.dimensions, - 'attrs': {a: getattr(obj, a) for a in obj.ncattrs()} - } - return scheme - - -def coordinate_variables(scheme): - """In-memory coordinate variable detection""" - coords = set() - for item in scheme.items(): - attrs = item['attrs'] - if 'coordinates' not in attrs: - continue - for c in attrs['coordinates'].split(): - coords.add(c) - return coords - - -def dimension_variables(scheme): - """In-memory dimension variable detection""" - names = set() - for item in scheme.items(): - for d in scheme['dimensions']: - if d in scheme['variables']: - names.add(d) - return names - - -def load_variables(path, names): - values = {} - with netCDF4.Dataset(path) as dataset: - for name in names: - try: - var = dataset.variables[name] - except KeyError: - continue - if 'time' in name: - datetimes = netCDF4.num2date(var[:], units=var.units) - values[name] = np.array(datetimes, dtype='datetime64[s]') - else: - values[name] = var[:] - return values - - -class Locator(object): - def __init__(self, paths): - self.paths = np.asarray(paths) - self.initial_times = np.array([ - util.initial_time(p) for p in paths], - dtype='datetime64[s]') - - def search(self, initial): - if isinstance(initial, str): - initial = np.datetime64(initial, 's') - return self.paths[self.initial_times == initial] - - -class GlobalUM(object): - """Locator for collection of UM diagnostic files - - Uses file naming convention and meta-data stored in - files to quickly look up file/index related to point - in space/time +def ndindex(masks, axes): + """N-dimensional array indexing + + Given logical masks and their axes generate + a multi-dimensional slice + + :returns: tuple(slices) """ - def __init__(self, paths): - self.locator = Locator(paths) - self.paths = np.asarray(paths) - self.valid_times = {} - self.pressures = {} - for path in paths: - with netCDF4.Dataset(path) as dataset: - var = dataset.variables["time"] - dates = netCDF4.num2date(var[:], - units=var.units) - if isinstance(dates, dt.datetime): - dates = np.array([dates], dtype=object) - self.valid_times[path] = dates.astype( - 'datetime64[s]') - self.pressures[path] = dataset.variables[ - 'pressure'][:] - - def search(self, *args, **kwargs): - return self.path_points(*args, **kwargs) - - def path_index(self, variable, initial, valid, pressure): - path, pts = self.path_points(variable, initial, valid, pressure) - return path, np.argmax(pts) - - def path_points(self, variable, initial, valid, pressure): - if isinstance(valid, str): - valid = np.datetime64(valid, 's') - paths = self.run_paths(initial) - for path in paths: - with netCDF4.Dataset(path) as dataset: - valid_times = self._valid_times(dataset, variable) - pressures = self._pressures(dataset, variable) - if pressures is not None: - pts = points( - valid_times, - pressures, - valid, - pressure) - else: - pts = time_points( - valid_times, - valid) - if pts.any(): - return path, pts - raise NoData('initial: {} valid: {} pressure: {}'.format(initial, valid, pressure)) - - @staticmethod - def _pressures(dataset, variable): - """Search dataset for pressure axis""" - var = dataset.variables[variable] - for d in var.dimensions: - if d.startswith('pressure'): - if d in dataset.variables: - return dataset.variables[d][:] - coords = var.coordinates.split() - for c in coords: - if c.startswith('pressure'): - return dataset.variables[c][:] - - @staticmethod - def _valid_times(dataset, variable): - """Search dataset for time axis""" - var = dataset.variables[variable] - for d in var.dimensions: - if d.startswith('time'): - if d in dataset.variables: - tvar = dataset.variables[d] - return np.array( - netCDF4.num2date(tvar[:], units=tvar.units), - dtype='datetime64[s]') - coords = var.coordinates.split() - for c in coords: - if c.startswith('time'): - tvar = dataset.variables[c] - return np.array( - netCDF4.num2date(tvar[:], units=tvar.units), - dtype='datetime64[s]') - - def run_paths(self, initial): - return self.locator.search(initial) - - - -def file_name(pattern, initial, length): - if isinstance(initial, np.datetime64): - initial = initial.astype(dt.datetime) - if isinstance(length, np.timedelta64): - length = length / np.timedelta64(1, 'h') - return pattern.format(initial, int(length)) - - -def lengths(times, initial): - if isinstance(initial, dt.datetime): - initial = np.datetime64(initial, 's') - if times.dtype == 'O': - times = times.astype('datetime64[s]') - return (times - initial) / np.timedelta64(1, 'h') - - -def points(times, pressures, time, pressure): - """Locate slice of array for time/pressure""" - return ( - pressure_points(pressures, pressure) & - time_points(times, time)) - - -def pressure_points(pressures, pressure): - ptol = 1 - return np.abs(pressures - pressure) < ptol - - -def time_points(times, time): - ttol = np.timedelta64(15 * 60, 's') - if times.dtype == 'O': - times = times.astype('datetime64[s]') - if isinstance(time, dt.datetime): + joint = {} + for mask, axis in zip(masks, axes): + if axis in joint: + joint[axis] = joint[axis] & mask + else: + joint[axis] = mask + rank = max(joint.keys()) + 1 # find highest dimension + return axes_pts([joint[i] for i in range(rank)]) + + +def axes_pts(masks): + slices = [] + for mask in masks: + pts = np.where(mask)[0][0] + slices.append(pts) + return tuple(slices) + + +def coord_mask(name, values, value): + return { + "time": time_mask, + "pressure": pressure_mask}[name](values, value) + + +def time_mask(times, time): + """Logical mask that selects particular time""" + if isinstance(time, (str, dt.datetime)): time = np.datetime64(time, 's') - return np.abs(times - time) < ttol + if isinstance(times, list): + times = np.array(times, dtype='datetime64[s]') + return times == time + + +def pressure_mask(pressures, pressure, rtol=0.01): + """Logical mask that selects particular pressure""" + if isinstance(pressures, list): + pressures = np.array(pressures, dtype='d') + return (np.abs(pressures - pressure) / np.abs(pressure)) < rtol + + +def pressure_axis(path, variable): + return _axis("pressure", path, variable) + + +def time_axis(path, variable): + return _axis("time", path, variable) + + +def _axis(name, path, variable): + dims, coords = load_dim_coords(path, variable) + value = axis(name, dims, coords) + if value is None: + msg = "{} axis not found: '{}' '{}'".format(name.capitalize(), path, variable) + raise AxisNotFound(msg) + else: + return value + + +def load_dim_coords(path, variable): + with netCDF4.Dataset(path) as dataset: + var = dataset.variables[variable] + dims = var.dimensions + coords = getattr(var, "coordinates", "") + return dims, coords + + +def has_coord(coord, dims, coords): + return coord_var(coord, dims, coords) is not None + + +def coord_var(coord, dims, coords): + for d in dims: + if d.startswith(coord): + return d + for c in coords.split(): + if c.startswith(coord): + return c -def pressure_index(pressures, pressure): - return np.argmin(np.abs(pressures - pressure)) +def axis(name, dims, coords): + for i, d in enumerate(dims): + if d.startswith(name): + return i + for c in coords.split(): + if c.startswith(name): + return 0 diff --git a/forest/earth_networks.py b/forest/earth_networks.py index 6c1652bcd..cb1bef7b0 100644 --- a/forest/earth_networks.py +++ b/forest/earth_networks.py @@ -1,6 +1,6 @@ import datetime as dt import pandas as pd -import forest.geo as geo +from forest import geo import bokeh.models diff --git a/forest/eida50.py b/forest/eida50.py new file mode 100644 index 000000000..c9777d65d --- /dev/null +++ b/forest/eida50.py @@ -0,0 +1,25 @@ +import netCDF4 +import numpy as np +from functools import lru_cache + + +class Coordinates(object): + """Coordinate system related to EIDA50 file(s)""" + def initial_time(self, path): + return min(self._cached_times(path)) + + def valid_times(self, path, variable): + return self._cached_times(path) + + @lru_cache() + def _cached_times(self, path): + with netCDF4.Dataset(path) as dataset: + var = dataset.variables["time"] + values = netCDF4.num2date(var[:], units=var.units) + return np.array(values, dtype='datetime64[s]') + + def variables(self, path): + return ["EIDA50"] + + def pressures(self, path, variable): + pass diff --git a/forest/exceptions.py b/forest/exceptions.py index 7c28e2f6e..15b9cb6f1 100644 --- a/forest/exceptions.py +++ b/forest/exceptions.py @@ -4,3 +4,19 @@ class FileNotFound(Exception): class IndexNotFound(Exception): pass + + +class PressuresNotFound(Exception): + pass + + +class InitialTimeNotFound(Exception): + pass + + +class ValidTimesNotFound(Exception): + pass + + +class SearchFail(Exception): + pass diff --git a/forest/geo.py b/forest/geo.py index d77b816d8..9af13b696 100644 --- a/forest/geo.py +++ b/forest/geo.py @@ -1,4 +1,8 @@ -import cartopy +try: + import cartopy +except ImportError: + # ReadTheDocs unable to pip install cartopy + pass import numpy as np import scipy.interpolate import scipy.ndimage diff --git a/forest/main.py b/forest/main.py index 893d2e1dc..76c9f9ff0 100644 --- a/forest/main.py +++ b/forest/main.py @@ -2,28 +2,36 @@ import bokeh.events import numpy as np import os -import forest.satellite as satellite -import forest.data as data -import forest.view as view -import forest.images as images -import forest.earth_networks as earth_networds -import forest.rdt as rdt -import forest.geo as geo -import forest.colors as colors -import forest.db as db +from forest import ( + satellite, + data, + view, + images, + earth_networks, + rdt, + geo, + colors, + db, + unified_model, + navigate, + parse_args) import forest.config as cfg -import forest.parse_args as parse_args from forest.util import Observable from forest.db.util import autolabel import datetime as dt -def main(): - args = parse_args.parse_args() - if args.database != ':memory:': - assert os.path.exists(args.database), "{} must exist".format(args.database) - database = db.Database.connect(args.database) - config = cfg.load_config(args.config_file) +def main(argv=None): + args = parse_args.parse_args(argv) + if len(args.files) > 0: + config = cfg.from_files(args.files, args.file_type) + else: + config = cfg.load_config(args.config_file) + + if args.database is not None: + if args.database != ':memory:': + assert os.path.exists(args.database), "{} must exist".format(args.database) + database = db.Database.connect(args.database) # Full screen map lon_range = (0, 30) @@ -110,24 +118,33 @@ def replace_dir(args_dir, group_dir): return os.path.join(args_dir, group_dir) for group in config.file_groups: + print(group) if group.label not in data.LOADERS: if group.locator == "database": locator = db.Locator( database.connection, directory=replace_dir(args.directory, group.directory)) loader = data.DBLoader(group.label, group.pattern, locator) - data.add_loader(group.label, loader) elif group.locator == "file_system": - if args.directory is not None: - pattern = os.path.join(args.directory, group.pattern) + if group.file_type == 'unified_model': + locator = unified_model.Locator(args.files) + loader = data.DBLoader(group.label, group.pattern, locator) else: - pattern = group.pattern - loader = data.file_loader( - group.file_type, - pattern) - data.add_loader(group.label, loader) + if args.directory is not None: + pattern = os.path.join(args.directory, group.pattern) + else: + if group.directory is None: + pattern = group.pattern + else: + pattern = os.path.join( + os.path.expanduser(group.directory), + group.pattern) + loader = data.file_loader( + group.file_type, + pattern) else: raise Exception("Unknown locator: {}".format(group.locator)) + data.add_loader(group.label, loader) renderers = {} viewers = {} @@ -243,24 +260,50 @@ def on_change(attr, old, new): bokeh.layouts.column(div), bokeh.layouts.column(dropdown)) - # Pre-select menu choices (if any) - state = None - for _, pattern in config.patterns: - state = db.initial_state(database, pattern=pattern) - break - # Pre-select first layer for name, _ in config.patterns: image_controls.select(name) break - # Add prototype database controls - controls = db.Controls(database, patterns=config.patterns, state=state) - controls.subscribe(controls.render) - controls.subscribe(artist.on_state) + if len(args.files) > 0: + navigator = navigate.FileSystem.file_type( + args.files, + args.file_type) + elif args.database is not None: + navigator = database + else: + navigator = navigate.Config(config) + + # Pre-select menu choices (if any) + initial_state = {} + for _, pattern in config.patterns: + initial_state = db.initial_state(navigator, pattern=pattern) + break + middlewares = [ + db.Log(verbose=True), + db.InverseCoordinate("pressure"), + db.next_previous, + db.Controls(navigator), + db.Converter({ + "valid_times": db.stamps, + "inital_times": db.stamps + }) + ] + store = db.Store( + db.reducer, + initial_state=initial_state, + middlewares=middlewares) + controls = db.ControlView() + controls.subscribe(store.dispatch) + store.subscribe(controls.render) + old_states = (db.Stream() + .listen_to(store) + .map(lambda x: db.State(**x))) + old_states.subscribe(artist.on_state) # Ensure all listeners are pointing to the current state - controls.notify(controls.state) + store.notify(store.state) + store.dispatch(db.set_value("patterns", config.patterns)) tabs = bokeh.models.Tabs(tabs=[ bokeh.models.Panel( @@ -315,7 +358,7 @@ def cb(event): series_figure, config.file_groups, directory=args.directory) - controls.subscribe(series.on_state) + old_states.subscribe(series.on_state) for f in figures: f.on_event(bokeh.events.Tap, series.on_tap) f.on_event(bokeh.events.Tap, place_marker(f, marker_source)) diff --git a/forest/navigate.py b/forest/navigate.py new file mode 100644 index 000000000..de04a2cac --- /dev/null +++ b/forest/navigate.py @@ -0,0 +1,118 @@ +import numpy as np +import fnmatch +import glob +import os +from .exceptions import ( + InitialTimeNotFound, + ValidTimesNotFound, + PressuresNotFound) +from . import ( + unified_model, + eida50, + rdt) + + +class Config(object): + """File system navigation using config file + + This implementation performs a glob and then delegates to + FileSystem navigators + """ + def __init__(self, config): + self.config = config + self.navigators = {} + for group in self.config.file_groups: + if group.directory is None: + pattern = group.pattern + else: + pattern = os.path.join(group.directory, group.pattern) + paths = glob.glob(os.path.expanduser(pattern)) + self.navigators[group.pattern] = FileSystem.file_type(paths, group.file_type) + + def variables(self, pattern): + print(pattern) + return self.navigators[pattern].variables(pattern) + + def initial_times(self, pattern, variable=None): + return self.navigators[pattern].initial_times(pattern, variable=variable) + + def valid_times(self, pattern, variable, initial_time): + return self.navigators[pattern].valid_times(pattern, variable, initial_time) + + def pressures(self, pattern, variable, initial_time): + return self.navigators[pattern].pressures(pattern, variable, initial_time) + + +class FileSystem(object): + """Navigates collections of file(s) + + .. note:: This is a naive implementation designed + to support basic command line file usage + """ + def __init__(self, paths, coordinates=None): + self.paths = paths + if coordinates is None: + coordinates = unified_model.Coordinates() + self.coordinates = coordinates + + @classmethod + def file_type(cls, paths, file_type): + if file_type.lower() == "rdt": + coordinates = rdt.Coordinates() + elif file_type.lower() == "eida50": + coordinates = eida50.Coordinates() + elif file_type.lower() == "unified_model": + coordinates = unified_model.Coordinates() + else: + raise Exception("Unrecognised file type: '{}'".format(file_type)) + return cls(paths, coordinates) + + def variables(self, pattern): + paths = fnmatch.filter(self.paths, pattern) + names = [] + for path in paths: + names += self.coordinates.variables(path) + return list(sorted(set(names))) + + def initial_times(self, pattern, variable=None): + paths = fnmatch.filter(self.paths, pattern) + times = [] + for path in paths: + try: + time = self.coordinates.initial_time(path) + if time is None: + continue + times.append(time) + except InitialTimeNotFound: + pass + return list(sorted(set(times))) + + def valid_times(self, pattern, variable, initial_time): + paths = fnmatch.filter(self.paths, pattern) + arrays = [] + for path in paths: + try: + array = self.coordinates.valid_times(path, variable) + if array is None: + continue + arrays.append(array) + except ValidTimesNotFound: + pass + if len(arrays) == 0: + return [] + return np.unique(np.concatenate(arrays)) + + def pressures(self, pattern, variable, initial_time): + paths = fnmatch.filter(self.paths, pattern) + arrays = [] + for path in paths: + try: + array = self.coordinates.pressures(path, variable) + if array is None: + continue + arrays.append(array) + except PressuresNotFound: + pass + if len(arrays) == 0: + return [] + return np.unique(np.concatenate(arrays)) diff --git a/forest/observe.py b/forest/observe.py new file mode 100644 index 000000000..7c94246b4 --- /dev/null +++ b/forest/observe.py @@ -0,0 +1,12 @@ + + +class Observer(object): + def __init__(self): + self.subscribers = [] + + def subscribe(self, method): + self.subscribers.append(method) + + def notify(self, value): + for method in self.subscribers: + method(value) diff --git a/forest/parse_args.py b/forest/parse_args.py index deda5801f..b5c69e07f 100644 --- a/forest/parse_args.py +++ b/forest/parse_args.py @@ -3,15 +3,30 @@ def parse_args(argv=None): parser = argparse.ArgumentParser() + add_arguments(parser) + args = parser.parse_args(args=argv) + if ( + (args.config_file is None) and + (len(args.files) == 0)): + msg = "Either specify file(s) or --config-file" + parser.error(msg) + return args + + +def add_arguments(parser): parser.add_argument( "--directory", help="directory to use with paths returned from database") parser.add_argument( "--database", - required=True, help="SQL database to optimise menu system") parser.add_argument( "--config-file", - required=True, metavar="YAML_FILE", + metavar="YAML_FILE", help="YAML file to configure application") - return parser.parse_args(args=argv) + parser.add_argument( + "files", nargs="*", metavar="FILE", + help="FILE(s) to display") + parser.add_argument( + "--file-type", default="unified_model", metavar="FILETYPE", + help="keyword to navigate/display file(s)") diff --git a/forest/rdt.py b/forest/rdt.py index a4e68d159..d7ffbb64f 100644 --- a/forest/rdt.py +++ b/forest/rdt.py @@ -5,8 +5,9 @@ import bokeh import json import numpy as np -import forest.geo as geo -import forest.locate as locate +from forest import ( + geo, + locate) from forest.util import timeout_cache from forest.exceptions import FileNotFound @@ -66,10 +67,11 @@ def __init__(self, loader): def render(self, state): if state.valid_time is not None: date = dt.datetime.strptime(state.valid_time, '%Y-%m-%d %H:%M:%S') - print(date) + print("rdt.View.render", date) try: self.source.geojson = self.loader.load_date(date) except FileNotFound: + print("rdt.View.render caught FileNotFound", date) self.source.geojson = self.empty_geojson def add_figure(self, figure): @@ -144,6 +146,7 @@ def load(path): class Locator(object): def __init__(self, pattern): + print("rdt.Locator('{}')".format(pattern)) self.pattern = pattern def find_file(self, valid_date): @@ -172,7 +175,29 @@ def dates(self, paths): self.parse_date(p) for p in paths], dtype='datetime64[s]') - def parse_date(self, path): + @staticmethod + def parse_date(path): groups = re.search(r"[0-9]{12}", os.path.basename(path)) if groups is not None: return dt.datetime.strptime(groups[0], "%Y%m%d%H%M") + + +class Coordinates(object): + """Menu system interface""" + def initial_time(self, path): + times = self.valid_times(path, None) + if len(times) > 0: + return times[0] + return None + + def variables(self, path): + return ["RDT"] + + def valid_times(self, path, variable): + date = Locator.parse_date(path) + if date is None: + return [] + return [str(date)] + + def pressures(self, path, variable): + return None diff --git a/forest/satellite.py b/forest/satellite.py index 399fe564e..331462a0e 100644 --- a/forest/satellite.py +++ b/forest/satellite.py @@ -5,8 +5,9 @@ import os import glob from functools import lru_cache -import forest.geo as geo -import forest.locate as locate +from forest import ( + geo, + locate) from forest.util import coarsify from forest.exceptions import FileNotFound, IndexNotFound diff --git a/forest/test_db_app_main.py b/forest/test_db_app_main.py deleted file mode 100644 index e9881cc63..000000000 --- a/forest/test_db_app_main.py +++ /dev/null @@ -1,17 +0,0 @@ -import unittest -import yaml -import forest.app.main as main - - -class TestMain(unittest.TestCase): - def test_main_loads_config(self): - content = yaml.dump({ - "models": [ - {"name": "Label", - "pattern": "*.nc"} - ] - }) - config = main.load_config(content) - result = config.patterns - expect = [("Label", "*.nc")] - self.assertEqual(expect, result) diff --git a/forest/test_db_app_parse_args.py b/forest/test_db_app_parse_args.py deleted file mode 100644 index 23ec9427e..000000000 --- a/forest/test_db_app_parse_args.py +++ /dev/null @@ -1,18 +0,0 @@ -import unittest -import forest.app.main as main - - -class TestApp(unittest.TestCase): - def test_parse_args_requires_database(self): - args = main.parse_args([ - "--database", "file.db", - "--config-file", "file.yaml" - ]) - self.assertEqual(args.database, "file.db") - - def test_parse_args_requires_config_file(self): - args = main.parse_args([ - "--database", "file.db", - "--config-file", "file.yaml" - ]) - self.assertEqual(args.config_file, "file.yaml") diff --git a/forest/test_db_control.py b/forest/test_db_control.py deleted file mode 100644 index 8fad3c55d..000000000 --- a/forest/test_db_control.py +++ /dev/null @@ -1,323 +0,0 @@ -import unittest -import unittest.mock -import datetime as dt -import forest.db as db - - -class TestControls(unittest.TestCase): - def setUp(self): - self.database = db.Database.connect(":memory:") - self.controls = db.Controls(self.database) - self.blank_message = db.Message('blank', None) - - def tearDown(self): - self.database.close() - - @unittest.skip("waiting on green light") - def test_on_change_emits_state(self): - key = "k" - value = "*.nc" - controls = db.Controls(self.database, patterns=[(key, value)]) - callback = unittest.mock.Mock() - controls.subscribe(callback) - controls.on_change('pattern')(None, None, value) - callback.assert_called_once_with(db.State(pattern=value)) - - def test_on_variable_emits_state(self): - value = "token" - callback = unittest.mock.Mock() - self.controls.subscribe(callback) - self.controls.on_change("variable")(None, None, value) - callback.assert_called_once_with(db.State(variable=value)) - - @unittest.skip("refactoring test suite") - def test_on_variable_sets_initial_times_drop_down(self): - self.controls.on_variable("air_temperature") - result = self.controls.dropdowns["initial_time"].menu - expect = [ - ("2019-01-01 00:00:00", "2019-01-01 00:00:00"), - ("2019-01-01 12:00:00", "2019-01-01 12:00:00"), - ] - self.assertEqual(expect, result) - - def test_next_state_given_kwargs(self): - current = db.State(pattern="p") - result = db.next_state(current, variable="v") - expect = db.State(pattern="p", variable="v") - self.assertEqual(expect, result) - - def test_observable(self): - state = db.State() - callback = unittest.mock.Mock() - self.controls.subscribe(callback) - self.controls.notify(state) - callback.assert_called_once_with(state) - - def test_render_state_configures_variable_menu(self): - controls = db.Controls(self.database) - state = db.State(variables=["mslp"]) - controls.render(state) - result = controls.dropdowns["variable"].menu - expect = ["mslp"] - self.assert_label_equal(expect, result) - self.assert_value_equal(expect, result) - - @unittest.skip("test-driven reducer development") - def test_send_sets_state_variables(self): - self.database.insert_variable("a.nc", "air_temperature") - self.database.insert_variable("b.nc", "mslp") - controls = db.Controls(self.database) - controls.state = db.State(pattern="b.nc") - controls.send(self.blank_message) - result = controls.state.variables - expect = ["mslp"] - self.assertEqual(expect, result) - - def test_render_state_configures_initial_time_menu(self): - initial_times = ["2019-01-01 12:00:00", "2019-01-01 00:00:00"] - state = db.State(initial_times=initial_times) - self.controls.render(state) - result = self.controls.dropdowns["initial_time"].menu - expect = initial_times - self.assert_label_equal(expect, result) - - @unittest.skip("test-driven reducer development") - def test_send_configures_initial_times(self): - for path, time in [ - ("a_0.nc", dt.datetime(2019, 1, 1)), - ("a_3.nc", dt.datetime(2019, 1, 1, 12))]: - self.database.insert_file_name(path, time) - self.controls.state = db.State(pattern="a_?.nc") - self.controls.send(self.blank_message) - result = self.controls.state.initial_times - expect = ["2019-01-01 12:00:00", "2019-01-01 00:00:00"] - self.assertEqual(expect, result) - - def test_render_given_initial_time_populates_valid_time_menu(self): - initial = dt.datetime(2019, 1, 1) - valid = dt.datetime(2019, 1, 1, 3) - self.database.insert_file_name("file.nc", initial) - self.database.insert_time("file.nc", "variable", valid, 0) - state = db.State() - message = db.Message.dropdown("initial_time", "2019-01-01 00:00:00") - new_state = self.controls.modify(state, message) - self.controls.render(new_state) - result = self.controls.dropdowns["valid_time"].menu - expect = ["2019-01-01 03:00:00"] - self.assert_label_equal(expect, result) - - def test_render_sets_pressure_levels(self): - pressures = [1000, 950, 850] - self.controls.render(db.State(pressures=pressures)) - result = self.controls.dropdowns["pressure"].menu - expect = ["1000hPa", "950hPa", "850hPa"] - self.assert_label_equal(expect, result) - - @unittest.skip("test-driven reducer development") - def test_modify_pressure_levels(self): - initial_time = "2019-01-01 00:00:00" - pressures = [1000., 950., 850.] - self.database.insert_file_name("file.nc", initial_time) - for i, value in enumerate(pressures): - self.database.insert_pressure("file.nc", "variable", value, i) - state = self.controls.modify( - db.State(initial_time=initial_time), - self.blank_message) - result = state.pressures - expect = pressures - self.assertEqual(expect, result) - - def test_next_pressure_given_pressures_returns_first_element(self): - value = 950 - controls = db.Controls( - self.database, - state=db.State(pressures=[value])) - controls.on_click('pressure', 'next')() - result = controls.state.pressure - expect = value - self.assertEqual(expect, result) - - def test_next_pressure_given_pressures_none(self): - controls = db.Controls( - self.database, - state=db.State()) - controls.on_click('pressure', 'next')() - result = controls.state.pressure - expect = None - self.assertEqual(expect, result) - - def test_next_pressure_given_current_pressure(self): - pressure = 950 - pressures = [1000, 950, 800] - controls = db.Controls( - self.database, - state=db.State( - pressures=pressures, - pressure=pressure) - ) - controls.on_click('pressure', 'next')() - result = controls.state.pressure - expect = 800 - self.assertEqual(expect, result) - - def test_render_given_pressure(self): - self.controls.render(db.State( - pressures=[1000], - pressure=1000)) - result = self.controls.dropdowns["pressure"].label - expect = "1000hPa" - self.assertEqual(expect, result) - - def test_hpa_given_small_pressures(self): - result = db.Controls.hpa(0.001) - expect = "0.001hPa" - self.assertEqual(expect, result) - - def test_render_given_no_variables_disables_dropdown(self): - self.controls.render(db.State(variables=[])) - result = self.controls.dropdowns["variable"].disabled - expect = True - self.assertEqual(expect, result) - - def test_render_variables_given_none_disables_dropdown(self): - self.controls.render(db.State(variables=None)) - result = self.controls.dropdowns["variable"].disabled - expect = True - self.assertEqual(expect, result) - - def test_render_initial_times_disables_buttons(self): - key = "initial_time" - self.controls.render(db.State(initial_times=None)) - self.assertEqual(self.controls.dropdowns[key].disabled, True) - self.assertEqual(self.controls.buttons[key]["next"].disabled, True) - self.assertEqual(self.controls.buttons[key]["previous"].disabled, True) - - def test_render_initial_times_enables_buttons(self): - key = "initial_time" - self.controls.render(db.State(initial_times=["2019-01-01 00:00:00"])) - self.assertEqual(self.controls.dropdowns[key].disabled, False) - self.assertEqual(self.controls.buttons[key]["next"].disabled, False) - self.assertEqual(self.controls.buttons[key]["previous"].disabled, False) - - def test_render_valid_times_given_none_disables_buttons(self): - state = db.State(valid_times=None) - self.check_disabled("valid_time", state, True) - - def test_render_valid_times_given_empty_list_disables_buttons(self): - state = db.State(valid_times=[]) - self.check_disabled("valid_time", state, True) - - def test_render_valid_times_given_values_enables_buttons(self): - state = db.State(valid_times=["2019-01-01 00:00:00"]) - self.check_disabled("valid_time", state, False) - - def test_render_pressures_given_none_disables_buttons(self): - state = db.State(pressures=None) - self.check_disabled("pressure", state, True) - - def test_render_pressures_given_empty_list_disables_buttons(self): - state = db.State(pressures=[]) - self.check_disabled("pressure", state, True) - - def test_render_pressures_given_values_enables_buttons(self): - state = db.State(pressures=[1000.00000001]) - self.check_disabled("pressure", state, False) - - def check_disabled(self, key, state, expect): - self.controls.render(state) - self.assertEqual(self.controls.dropdowns[key].disabled, expect) - self.assertEqual(self.controls.buttons[key]["next"].disabled, expect) - self.assertEqual(self.controls.buttons[key]["previous"].disabled, expect) - - def assert_label_equal(self, expect, result): - result = [l for l, _ in result] - self.assertEqual(expect, result) - - def assert_value_equal(self, expect, result): - result = [v for _, v in result] - self.assertEqual(expect, result) - - -class TestMessage(unittest.TestCase): - def setUp(self): - self.database = db.Database.connect(":memory:") - - def tearDown(self): - self.database.close() - - def test_state_change_given_dropdown_message(self): - state = db.State() - message = db.Message.dropdown("pressure", "1000") - result = db.Controls(self.database).modify(state, message) - expect = db.State(pressure=1000.) - self.assertEqual(expect, result) - - def test_state_change_given_previous_initial_time_message(self): - state = db.State(initial_times=["2019-01-01 00:00:00"]) - message = db.Message.button("initial_time", "previous") - result = db.Controls(self.database).modify(state, message) - expect = db.State( - initial_times=["2019-01-01 00:00:00"], - initial_time="2019-01-01 00:00:00") - self.assertEqual(expect.initial_time, result.initial_time) - self.assertEqual(expect.initial_times, result.initial_times) - self.assertEqual(expect, result) - - -class TestNextPrevious(unittest.TestCase): - def setUp(self): - self.initial_times = [ - "2019-01-02 00:00:00", - "2019-01-01 00:00:00", - "2019-01-04 00:00:00", - "2019-01-03 00:00:00", - ] - self.state = db.State(initial_times=self.initial_times) - self.store = db.Store(self.state) - - def test_next_given_none_selects_latest_time(self): - message = db.Message.button("initial_time", "next") - self.store.dispatch(message) - result = self.store.state - expect = db.State( - initial_time="2019-01-04 00:00:00", - initial_times=self.initial_times - ) - self.assert_state_equal(expect, result) - - def test_reducer_next_given_time_moves_forward_in_time(self): - message = db.Message.button("initial_time", "next") - state = db.State( - initial_time="2019-01-01 00:00:00", - initial_times=[ - "2019-01-01 00:00:00", - "2019-01-01 02:00:00", - "2019-01-01 01:00:00", - ]) - result = db.reducer(state, message) - expect = db.next_state(state, initial_time="2019-01-01 01:00:00") - self.assert_state_equal(expect, result) - - def test_previous_given_none_selects_earliest_time(self): - message = db.Message.button("initial_time", "previous") - self.store.dispatch(message) - result = self.store.state - expect = db.State( - initial_time="2019-01-01 00:00:00", - initial_times=self.initial_times - ) - self.assert_state_equal(expect, result) - - def test_next_item_given_last_item_returns_first_item(self): - result = db.control.next_item([0, 1, 2], 2) - expect = 0 - self.assertEqual(expect, result) - - def test_previous_item_given_first_item_returns_last_item(self): - result = db.control.previous_item([0, 1, 2], 0) - expect = 2 - self.assertEqual(expect, result) - - def assert_state_equal(self, expect, result): - for k, v in expect._asdict().items(): - self.assertEqual(v, getattr(result, k), k) diff --git a/forest/test_db_navigate.py b/forest/test_db_navigate.py deleted file mode 100644 index 4403d6c53..000000000 --- a/forest/test_db_navigate.py +++ /dev/null @@ -1,132 +0,0 @@ -import unittest -import datetime as dt -from db import Observable - - -class InvalidTime(Exception): - pass - - -class Control(Observable): - def __init__(self, values, value=None): - self.values = values - self.value = value - super().__init__() - - @property - def current(self): - return self.value - - def forward(self): - if len(self.values) == 0: - return - if self.value is None: - value = self.values[0] - else: - value = self.next_item(self.values, self.value) - self.notify(value) - self.value = value - - def backward(self): - if len(self.values) == 0: - return - if self.value is None: - value = self.values[-1] - else: - try: - value = self.previous_item(self.values, self.value) - except InvalidTime: - return - self.notify(value) - self.value = value - - def reset(self, values): - """Change available values to iterate over""" - - @staticmethod - def next_item(values, value): - index = values.index(value) + 1 - try: - return values[index] - except IndexError: - raise InvalidTime('outside: {}'.format(values[-1])) - - @staticmethod - def previous_item(values, value): - index = values.index(value) - 1 - if index < 0: - raise InvalidTime('outside: {}'.format(values[0])) - return values[index] - - -class TestTimeNavigation(unittest.TestCase): - def setUp(self): - self.times = [ - dt.datetime(2019, 1, 1, 12), - dt.datetime(2019, 1, 2, 0), - dt.datetime(2019, 1, 2, 12) - ] - self.control = Control(self.times, self.times[0]) - - def test_reset(self): - control = Control([1, 2, 3]) - control.reset([3, 4, 5]) - - def test_next_item(self): - result = self.control.next_item(self.times, self.times[1]) - expect = self.times[2] - self.assertEqual(expect, result) - - def test_next_raises_exception_if_outside_range(self): - with self.assertRaises(InvalidTime): - self.control.next_item(self.times, self.times[-1]) - - def test_previous_item(self): - result = self.control.previous_item(self.times, self.times[1]) - expect = self.times[0] - self.assertEqual(expect, result) - - def test_previous_raises_exception_if_outside_range(self): - with self.assertRaises(InvalidTime): - self.control.previous_item(self.times, self.times[0]) - - def test_observable(self): - listener = unittest.mock.Mock() - self.control.subscribe(listener) - self.control.forward() - listener.assert_called_once_with(self.times[1]) - - def test_backward_calls_listener(self): - listener = unittest.mock.Mock() - self.control.value = self.times[2] - self.control.subscribe(listener) - self.control.backward() - listener.assert_called_once_with(self.times[1]) - - def test_backward_given_time_at_start_of_series_does_nothing(self): - listener = unittest.mock.Mock() - self.control.subscribe(listener) - self.control.backward() - self.assertFalse(listener.called) - - def test_forward_if_current_not_set_selects_first_item(self): - control = Control([1, 2, 3]) - control.forward() - result = control.current - expect = 1 - self.assertEqual(expect, result) - - def test_forward_given_empty_list_does_nothing(self): - control = Control([]) - control.forward() - - def test_backward_given_none_sets_last_item(self): - control = Control([1, 2, 3]) - control.backward() - result = control.current - expect = 3 - self.assertEqual(expect, result) - - def test_backward_given_empty_list_does_nothing(self): - control = Control([]) - control.backward() diff --git a/forest/test_disk.py b/forest/test_disk.py deleted file mode 100644 index bd56d9348..000000000 --- a/forest/test_disk.py +++ /dev/null @@ -1,79 +0,0 @@ -import unittest -import datetime as dt -import numpy as np -import netCDF4 -import os -import disk - - -def full_path(name): - return os.path.join(os.path.dirname(__file__), name) - - -def stash_variables(dataset): - """Find all variables with Stash codes""" - return [name for name, obj in dataset.variables.items() - if hasattr(obj, 'um_stash_source')] - - -def pressures(dataset, name): - variable = dataset.variables[name] - dimensions = variable.dimensions - return dimensions - - -class TestPattern(unittest.TestCase): - def test_pattern_given_initial_time_and_length(self): - initial = np.datetime64('2019-04-29 18:00', 's') - length = np.timedelta64(33, 'h') - pattern = "global_africa_{:%Y%m%dT%H%MZ}_umglaa_pa{:03d}.nc" - result = disk.file_name(pattern, initial, length) - expect = "global_africa_20190429T1800Z_umglaa_pa033.nc" - self.assertEqual(expect, result) - - -class TestLocator(unittest.TestCase): - def test_paths_given_date_outside_range_returns_empty_list(self): - locator = disk.Locator([ - "20190101T0000Z.nc", - "20190101T0600Z.nc", - "20190101T1200Z.nc", - "20190101T1800Z.nc", - "20190102T0000Z.nc" - ]) - after = np.datetime64('2019-01-02 06:00', 's') - result = locator.search(after) - self.assertEqual(result.tolist(), []) - - def test_paths_given_date_in_range_returns_list(self): - locator = disk.Locator([ - "a/prefix_20190101T0000Z.nc", - "b/prefix_20190101T0600Z.nc", - "c/prefix_20190101T1200Z.nc", - "d/prefix_20190101T1800Z.nc", - "e/prefix_20190102T0000Z.nc" - ]) - time = np.datetime64('2019-01-01 18:00', 's') - result = locator.search(time) - expect = ["d/prefix_20190101T1800Z.nc"] - np.testing.assert_array_equal(expect, result) - - def test_paths_given_date_matching_multiple_files(self): - locator = disk.Locator([ - "a/prefix_20190101T0000Z.nc", - "b/prefix_20190101T0600Z.nc", - "c/prefix_20190101T1200Z_000.nc", - "c/prefix_20190101T1200Z_003.nc", - "c/prefix_20190101T1200Z_006.nc", - "c/prefix_20190101T1200Z_009.nc", - "d/prefix_20190101T1800Z.nc", - "e/prefix_20190102T0000Z.nc" - ]) - result = locator.search('2019-01-01 12:00') - expect = [ - "c/prefix_20190101T1200Z_000.nc", - "c/prefix_20190101T1200Z_003.nc", - "c/prefix_20190101T1200Z_006.nc", - "c/prefix_20190101T1200Z_009.nc", - ] - np.testing.assert_array_equal(expect, result) diff --git a/forest/test_eida50.py b/forest/test_eida50.py deleted file mode 100644 index e71078a00..000000000 --- a/forest/test_eida50.py +++ /dev/null @@ -1,94 +0,0 @@ -import unittest -import os -import datetime as dt -import netCDF4 -import satellite -from exceptions import FileNotFound, IndexNotFound - - -class TestLocator(unittest.TestCase): - def setUp(self): - self.paths = [] - self.pattern = "test-eida50*.nc" - self.locator = satellite.Locator(self.pattern) - - def tearDown(self): - for path in self.paths: - if os.path.exists(path): - os.remove(path) - - def test_parse_date(self): - path = "/some/file-20190101.nc" - result = self.locator.parse_date(path) - expect = dt.datetime(2019, 1, 1) - self.assertEqual(expect, result) - - @unittest.skip('awaiting development') - def test_find_given_no_files_raises_notfound(self): - any_date = dt.datetime.now() - with self.assertRaises(FileNotFound): - self.locator.find(any_date) - - def test_find_given_a_single_file(self): - valid_date = dt.datetime(2019, 1, 1) - path = "test-eida50-20190101.nc" - self.paths.append(path) - - times = [valid_date] - with netCDF4.Dataset(path, "w") as dataset: - self.set_times(dataset, times) - - found_path, index = self.locator.find(valid_date) - self.assertEqual(found_path, path) - self.assertEqual(index, 0) - - def test_find_given_multiple_files(self): - dates = [ - dt.datetime(2019, 1, 1), - dt.datetime(2019, 1, 2), - dt.datetime(2019, 1, 3)] - for date in dates: - path = "test-eida50-{:%Y%m%d}.nc".format(date) - self.paths.append(path) - with netCDF4.Dataset(path, "w") as dataset: - self.set_times(dataset, [date]) - valid_date = dt.datetime(2019, 1, 2, 0, 14) - found_path, index = self.locator.find(valid_date) - expect_path = "test-eida50-20190102.nc" - self.assertEqual(found_path, expect_path) - self.assertEqual(index, 0) - - def test_find_index_given_valid_time(self): - time = dt.datetime(2019, 1, 1, 3, 31) - times = [ - dt.datetime(2019, 1, 1, 3, 0), - dt.datetime(2019, 1, 1, 3, 15), - dt.datetime(2019, 1, 1, 3, 30), - dt.datetime(2019, 1, 1, 3, 45), - dt.datetime(2019, 1, 1, 4, 0), - ] - freq = dt.timedelta(minutes=15) - result = self.locator.find_index(times, time, freq) - expect = 2 - self.assertEqual(expect, result) - - @unittest.skip('awaiting development') - def test_find_index_outside_range_raises_exception(self): - time = dt.datetime(2019, 1, 4, 16) - times = [ - dt.datetime(2019, 1, 1, 3, 0), - dt.datetime(2019, 1, 1, 3, 15), - dt.datetime(2019, 1, 1, 3, 30), - dt.datetime(2019, 1, 1, 3, 45), - dt.datetime(2019, 1, 1, 4, 0), - ] - freq = dt.timedelta(minutes=15) - with self.assertRaises(IndexNotFound): - self.locator.find_index(times, time, freq) - - def set_times(self, dataset, times): - units = "seconds since 1970-01-01 00:00:00" - dataset.createDimension("time", len(times)) - var = dataset.createVariable("time", "d", ("time",)) - var.units = units - var[:] = netCDF4.date2num(times, units=units) diff --git a/forest/test_parse_args.py b/forest/test_parse_args.py deleted file mode 100644 index 4ff85dda7..000000000 --- a/forest/test_parse_args.py +++ /dev/null @@ -1,23 +0,0 @@ -import unittest -import parse_args - - -class TestParseArgs(unittest.TestCase): - def test_directory_returns_none_by_default(self): - args = parse_args.parse_args([ - "--database", "file.db", - "--config-file", "file.yml" - ]) - result = args.directory - expect = None - self.assertEqual(expect, result) - - def test_directory_returns_value(self): - args = parse_args.parse_args([ - "--directory", "/some", - "--database", "file.db", - "--config-file", "file.yml" - ]) - result = args.directory - expect = "/some" - self.assertEqual(expect, result) diff --git a/forest/tutorial/__init__.py b/forest/tutorial/__init__.py new file mode 100644 index 000000000..ecfa83220 --- /dev/null +++ b/forest/tutorial/__init__.py @@ -0,0 +1,5 @@ +""" +Sample data to run canned versions of FOREST +""" +from .core import * +from . import main diff --git a/forest/tutorial/core.py b/forest/tutorial/core.py new file mode 100644 index 000000000..67424f746 --- /dev/null +++ b/forest/tutorial/core.py @@ -0,0 +1,162 @@ +import os +import shutil +import datetime as dt +import netCDF4 +import numpy as np +import forest.db + + +SOURCE_DIR = os.path.dirname(__file__) +CFG_FILE = "config.yaml" +UM_FILE = "unified_model.nc" +DB_FILE = "database.db" +RDT_FILE = "rdt_201904171245.json" +EIDA50_FILE = "eida50_20190417.nc" + + +def build_all(build_dir): + """Build sample files""" + for builder in [ + build_config, + build_rdt, + build_eida50, + build_um, + build_database]: + builder(build_dir) + + +def build_rdt(build_dir): + build_file(build_dir, RDT_FILE) + + +def build_eida50(build_dir): + build_file(build_dir, EIDA50_FILE) + + +def build_file(directory, file_name): + src = os.path.join(SOURCE_DIR, file_name) + dst = os.path.join(directory, file_name) + print("copying: {} to {}".format(src, dst)) + shutil.copy2(src, dst) + + +def build_config(build_dir): + path = os.path.join(build_dir, CFG_FILE) + content = """ + files: + - label: Unified Model + pattern: "*{}" + directory: {} + locator: database +""".format(UM_FILE, build_dir) + print("writing: {}".format(path)) + with open(path, "w") as stream: + stream.write(content) + + +def build_um(build_dir): + nx, ny = 100, 100 + x = np.linspace(0, 45, nx) + y = np.linspace(0, 45, ny) + X, Y = np.meshgrid(x, y) + Z = np.sqrt(X**2 + Y**2) + times = [dt.datetime(2019, 1, 1), dt.datetime(2019, 1, 2)] + path = os.path.join(build_dir, UM_FILE) + print("writing: {}".format(path)) + with netCDF4.Dataset(path, "w") as dataset: + formatter = UM(dataset) + var = formatter.longitudes(nx) + var[:] = x + var = formatter.latitudes(ny) + var[:] = y + var = formatter.times("time", length=len(times), dim_name="dim0") + var[:] = netCDF4.date2num(times, units=var.units) + formatter.forecast_reference_time(times[0]) + var = formatter.pressures("pressure", length=len(times), dim_name="dim0") + var[:] = 1000. + dims = ("dim0", "longitude", "latitude") + coordinates = "forecast_period_1 forecast_reference_time pressure time" + var = formatter.relative_humidity(dims, coordinates=coordinates) + var[:] = Z.T + + +def build_database(build_dir): + db_path = os.path.join(build_dir, DB_FILE) + um_path = os.path.join(build_dir, UM_FILE) + if not os.path.exists(um_path): + build_um(build_dir) + print("building: {}".format(db_path)) + database = forest.db.Database.connect(db_path) + database.insert_netcdf(um_path) + database.close() + + +class UM(object): + """Unified model diagnostics formatter""" + def __init__(self, dataset): + self.dataset = dataset + self.units = "hours since 1970-01-01 00:00:00" + + def times(self, name, length=None, dim_name=None): + if dim_name is None: + dim_name = name + dataset = self.dataset + if dim_name not in dataset.dimensions: + dataset.createDimension(dim_name, length) + var = dataset.createVariable(name, "d", (dim_name,)) + var.axis = "T" + var.units = self.units + var.standard_name = "time" + var.calendar = "gregorian" + return var + + def forecast_reference_time(self, time, name="forecast_reference_time"): + dataset = self.dataset + var = dataset.createVariable(name, "d", ()) + var.units = self.units + var.standard_name = name + var.calendar = "gregorian" + var[:] = netCDF4.date2num(time, units=self.units) + + def pressures(self, name, length=None, dim_name=None): + if dim_name is None: + dim_name = name + dataset = self.dataset + if dim_name not in dataset.dimensions: + dataset.createDimension(dim_name, length) + var = dataset.createVariable(name, "d", (dim_name,)) + var.axis = "Z" + var.units = "hPa" + var.long_name = "pressure" + return var + + def longitudes(self, length=None, name="longitude"): + dataset = self.dataset + if name not in dataset.dimensions: + dataset.createDimension(name, length) + var = dataset.createVariable(name, "f", (name,)) + var.axis = "X" + var.units = "degrees_east" + var.long_name = "longitude" + return var + + def latitudes(self, length=None, name="latitude"): + dataset = self.dataset + if name not in dataset.dimensions: + dataset.createDimension(name, length) + var = dataset.createVariable(name, "f", (name,)) + var.axis = "Y" + var.units = "degrees_north" + var.long_name = "latitude" + return var + + def relative_humidity(self, dims, name="relative_humidity", + coordinates="forecast_period_1 forecast_reference_time"): + dataset = self.dataset + var = dataset.createVariable(name, "f", dims) + var.standard_name = "relative_humidity" + var.units = "%" + var.um_stash_source = "m01s16i204" + var.grid_mapping = "latitude_longitude" + var.coordinates = coordinates + return var diff --git a/forest/tutorial/eida50_20190417.nc b/forest/tutorial/eida50_20190417.nc new file mode 100644 index 000000000..4e75ffd60 Binary files /dev/null and b/forest/tutorial/eida50_20190417.nc differ diff --git a/forest/tutorial/main.py b/forest/tutorial/main.py new file mode 100644 index 000000000..8bacfe557 --- /dev/null +++ b/forest/tutorial/main.py @@ -0,0 +1,32 @@ +"""Command-line interface to forest-tutorial + +This script builds the sample file(s) needed to +follow along with the tutorial at: + +https://forest-informaticslab.readthedocs.io + +""" +import argparse +import os +from . import core + + +class HelpFormatter( + argparse.ArgumentDefaultsHelpFormatter, + argparse.RawTextHelpFormatter): + pass + + +def parse_args(argv=None): + parser = argparse.ArgumentParser( + description=__doc__, + formatter_class=HelpFormatter) + parser.add_argument("build_dir", + metavar="BUILD_DIR", + help="directory in which to build sample files, e.g. '.'") + return parser.parse_args(args=argv) + + +def main(argv=None): + args = parse_args(argv=argv) + core.build_all(build_dir=args.build_dir) diff --git a/forest/sample/RDT_features_eastafrica_201904171245.json b/forest/tutorial/rdt_201904171245.json similarity index 100% rename from forest/sample/RDT_features_eastafrica_201904171245.json rename to forest/tutorial/rdt_201904171245.json diff --git a/forest/unified_model.py b/forest/unified_model.py new file mode 100644 index 000000000..2bbde2f41 --- /dev/null +++ b/forest/unified_model.py @@ -0,0 +1,246 @@ +import os +import re +import fnmatch +import datetime as dt +import numpy as np +import netCDF4 +from forest import disk +from forest.exceptions import SearchFail +try: + import iris +except ImportError: + # ReadTheDocs can't install iris + pass + + +class NotFound(Exception): + pass + + +class Coordinates(object): + """Coordinate system for unified model diagnostics""" + def initial_time(self, path): + return InitialTimeLocator()(path) + + def variables(self, path): + cubes = iris.load(path) + return [cube.name() for cube in cubes] + + def valid_times(self, path, variable): + return ValidTimesLocator()(path, variable) + + def pressures(self, path, variable): + return PressuresLocator()(path, variable) + + +class Locator(object): + def __init__(self, paths): + self.paths = paths + self.spare = [] + self.catalogue = {} + for path in paths: + initial_time = self.initial_time(path) + if initial_time is None: + self.spare.append(path) + continue + key = self.key(initial_time) + if key not in self.catalogue: + self.catalogue[key] = [path] + else: + self.catalogue[key].append(path) + + def locate( + self, + pattern, + variable, + initial_time, + valid_time, + pressure=None, + tolerance=0.001): + paths = self.find_paths(initial_time) + self.spare + paths = fnmatch.filter(paths, pattern) + for path in paths: + with netCDF4.Dataset(path) as dataset: + if variable not in dataset.variables: + continue + + var = dataset.variables[variable] + dims = var.dimensions + coords = getattr(var, "coordinates", "") + + masks = {} + for coord, value in [ + ("time", valid_time), + ("pressure", pressure)]: + if not disk.has_coord(coord, dims, coords): + continue + if value is None: + # Coordinate present but value not specified + raise SearchFail("Please specify: '{}'".format(coord)) + continue + axis = disk.axis(coord, dims, coords) + coord_var = disk.coord_var(coord, dims, coords) + if coord == "time": + obj = dataset.variables[coord_var] + values = netCDF4.num2date(obj[:], units=obj.units) + else: + values = dataset.variables[coord_var][:] + mask = disk.coord_mask(coord, values, value) + if axis not in masks: + masks[axis] = mask + else: + masks[axis] = masks[axis] & mask + + # Determine if search was successful + found = all(mask.any() for mask in masks.values()) + if not found: + continue + + # Generate multi-dimensional slice from search result + slices = [] + for i in range(max(masks.keys()) + 1): + pts = np.where(masks[i])[0][0] + slices.append(pts) + pts = tuple(slices) + return path, pts + + # Search failure message + msg = " ".join([str(value) for value in + [pattern, variable, initial_time, valid_time, pressure]]) + raise SearchFail(msg) + + def find_paths(self, initial_time): + return self.catalogue.get(self.key(initial_time), []) + + @staticmethod + def key(time): + if isinstance(time, str): + from dateutil import parser + time = parser.parse(time) + return time.strftime("%Y%m%dT%H%M%S") + + def initial_time(self, path): + for strategy in [ + self.initial_time_regex, + self.initial_time_netcdf4]: + result = strategy(path) + if result is None: + continue + else: + return result + + def initial_time_regex(self, path): + name = os.path.basename(path) + groups = re.search(r"[0-9]{8}T[0-9]{4}Z", path) + if groups: + return dt.datetime.strptime(groups[0], "%Y%m%dT%H%MZ") + + def initial_time_netcdf4(self, path): + with netCDF4.Dataset(path) as dataset: + try: + var = dataset.variables["forecast_reference_time"] + result = netCDF4.num2date(var[:], units=var.units) + except KeyError: + result = None + return result + + +class InitialTimeLocator(object): + def __call__(self, path): + try: + return self.netcdf4_strategy(path) + except KeyError: + return self.cube_strategy(path) + + @staticmethod + def netcdf4_strategy(path): + with netCDF4.Dataset(path) as dataset: + var = dataset.variables["forecast_reference_time" ] + values = netCDF4.num2date(var[:], units=var.units) + return values + + @staticmethod + def cube_strategy(path): + cubes = iris.load(path) + if len(cubes) > 0: + cube = cubes[0] + return cube.coord('time').cells().next().point + raise InitialTimeNotFound("No initial time: '{}'".format(path)) + + +class ValidTimesLocator(object): + def __call__(self, path, variable): + try: + t = self.netcdf4_strategy(path, variable) + except KeyError: + t = self.cube_strategy(path, variable) + if t is None: + t = self.cube_strategy(path, variable) + elif t.ndim == 0: + t = np.array([t], dtype='datetime64[s]') + return t + + def netcdf4_strategy(self, path, variable): + with netCDF4.Dataset(path) as dataset: + values = self._valid_times(dataset, variable) + return values + + @staticmethod + def _valid_times(dataset, variable): + """Search dataset for time axis""" + var = dataset.variables[variable] + for d in var.dimensions: + if d.startswith('time'): + if d in dataset.variables: + tvar = dataset.variables[d] + return np.array( + netCDF4.num2date(tvar[:], units=tvar.units), + dtype='datetime64[s]') + coords = var.coordinates.split() + for c in coords: + if c.startswith('time'): + tvar = dataset.variables[c] + return np.array( + netCDF4.num2date(tvar[:], units=tvar.units), + dtype='datetime64[s]') + + @staticmethod + def cube_strategy(path, variable): + cube = iris.load_cube(path, variable) + return np.array([ + c.point for c in cube.coord('time').cells()], + dtype='datetime64[s]') + + +class PressuresLocator(object): + def __call__(self, path, variable): + try: + return self.netcdf4_strategy(path, variable) + except KeyError: + return self.cube_strategy(path, variable) + + def cube_strategy(self, path, variable): + try: + cube = iris.load_cube(path, variable) + points = cube.coord('pressure').points + if np.ndim(points) == 0: + points = np.array([points]) + return points + except iris.exceptions.CoordinateNotFoundError: + raise PressuresNotFound("'{}' '{}'".format(path, variable)) + + @staticmethod + def netcdf4_strategy(path, variable): + """Search dataset for pressure axis""" + with netCDF4.Dataset(path) as dataset: + var = dataset.variables[variable] + for d in var.dimensions: + if d.startswith('pressure'): + if d in dataset.variables: + return dataset.variables[d][:] + coords = var.coordinates.split() + for c in coords: + if c.startswith('pressure'): + return dataset.variables[c][:] + # NOTE: refactor needed + raise KeyError diff --git a/forest/view.py b/forest/view.py index ee7317928..c0a143e30 100644 --- a/forest/view.py +++ b/forest/view.py @@ -1,6 +1,7 @@ import datetime as dt +import numpy as np import bokeh.models -import forest.geo as geo +from forest import geo from forest.exceptions import FileNotFound, IndexNotFound @@ -98,7 +99,21 @@ def __init__(self, loader, color_mapper): def render(self, state): print(state) if state.valid_time is not None: - self.image(dt.datetime.strptime(state.valid_time, '%Y-%m-%d %H:%M:%S')) + self.image(self.to_datetime(state.valid_time)) + + @staticmethod + def to_datetime(d): + if isinstance(d, dt.datetime): + return d + elif isinstance(d, str): + try: + return dt.datetime.strptime(d, "%Y-%m-%d %H:%M:%S") + except ValueError: + return dt.datetime.strptime(d, "%Y-%m-%dT%H:%M:%S") + elif isinstance(d, np.datetime64): + return d.astype(dt.datetime) + else: + raise Exception("Unknown value: {}".format(d)) def image(self, time): print("EIDA50: {}".format(time)) diff --git a/readthedocs.yml b/readthedocs.yml new file mode 100644 index 000000000..c0691d858 --- /dev/null +++ b/readthedocs.yml @@ -0,0 +1,7 @@ +version: 2 +sphinx: + configuration: doc/source/conf.py +python: + version: 3.7 + install: + - requirements: doc/requirements.txt diff --git a/setup.cfg b/setup.cfg index 3b9038cba..62ae24bcc 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,5 +1,5 @@ [tool:pytest] -testpaths = forest +testpaths = test addopts = -ra -v diff --git a/setup.py b/setup.py index c51f1345c..b5d403ecf 100644 --- a/setup.py +++ b/setup.py @@ -31,11 +31,12 @@ def load(fname): author_email="andrew.ryan@metoffice.gov.uk", description="Forecast visualisation and survey tool", packages=setuptools.find_packages(), - test_suite=NAME, + test_suite="test", tests_require=load("requirements-dev.txt"), entry_points={ 'console_scripts': [ 'forest=forest.cli.main:main', - 'forestdb=forest.db.main:main' + 'forestdb=forest.db.main:main', + 'forest-tutorial=forest.tutorial.main:main' ] }) diff --git a/test/sample/RDT_features_eastafrica_201904171245.json b/test/sample/RDT_features_eastafrica_201904171245.json new file mode 100644 index 000000000..11d16bac1 --- /dev/null +++ b/test/sample/RDT_features_eastafrica_201904171245.json @@ -0,0 +1 @@ +{"type":"FeatureCollection","features":[{"geometry":{"type":"Polygon","coordinates":[[[37.82,7.131],[37.939,7.132],[37.956,7.276],[37.877,7.275],[37.838,7.274],[37.795,7.245],[37.785,7.159],[37.82,7.131]]]},"type":"Feature","properties":{"NumIdBirth":0,"CType":9,"LonTrajCellCG":[37.8797],"CRainRate":20.1,"ConvTypeMethod":1,"ConvType":2,"SeverityIntensity":2,"ConvTypeQuality":3,"MvtDirection":"","CTPressure":10000.0,"NumIdCell":8,"CTReff":"","LonG":37.8797,"LatTrajCellCG":[7.2151],"ExpansionRate":"","CTPhase":"","LatG":7.2151,"BTmin":193.68,"BTmoy":207.25,"CTCot":"","CTCwp":"","NbPosLightning":0,"SeverityType":"","PhaseLife":1,"MvtSpeed":"","Surface":165.0,"Duration":0,"CoolingRate":-66.2904}},{"geometry":{"type":"Polygon","coordinates":[[[37.968,7.047],[38.086,7.049],[38.097,7.135],[38.021,7.162],[37.982,7.162],[37.942,7.161],[37.932,7.075],[37.968,7.047]]]},"type":"Feature","properties":{"NumIdBirth":31,"CType":9,"LonTrajCellCG":[38.0136,37.97,37.98,38.06,38.1,38.12],"CRainRate":19.5,"ConvTypeMethod":1,"ConvType":2,"SeverityIntensity":3,"ConvTypeQuality":3,"MvtDirection":276,"CTPressure":13800.0,"NumIdCell":9,"CTReff":"","LonG":38.0136,"LatTrajCellCG":[7.1002,7.15,7.16,7.15,7.16,7.16],"ExpansionRate":759.0241,"CTPhase":"","LatG":7.1002,"BTmin":206.0,"BTmoy":210.05,"CTCot":"","CTCwp":"","NbPosLightning":0,"SeverityType":"","PhaseLife":3,"MvtSpeed":4.36,"Surface":85.0,"Duration":4500,"CoolingRate":-66.2904}},{"geometry":{"type":"Polygon","coordinates":[[[38.746,6.572],[38.827,6.573],[38.91,6.602],[38.954,6.632],[38.967,6.746],[38.958,7.004],[38.884,7.06],[38.567,7.084],[38.487,7.083],[38.407,7.082],[38.328,7.081],[38.288,7.08],[38.245,7.051],[38.228,6.908],[38.373,6.795],[38.746,6.572]]]},"type":"Feature","properties":{"NumIdBirth":61,"CType":9,"LonTrajCellCG":[38.6512,38.66,38.77,38.78,38.78,38.78,38.81,38.61,38.59,38.58,38.59,38.65],"CRainRate":29.6,"ConvTypeMethod":1,"ConvType":2,"SeverityIntensity":0,"ConvTypeQuality":1,"MvtDirection":228,"CTPressure":11800.0,"NumIdCell":11,"CTReff":"","LonG":38.6512,"LatTrajCellCG":[6.8723,6.9,6.91,6.91,6.95,6.96,6.97,6.81,6.81,6.82,6.83,6.77],"ExpansionRate":-54.5759,"CTPhase":"","LatG":6.8723,"BTmin":200.28,"BTmoy":206.73,"CTCot":"","CTCwp":"","NbPosLightning":0,"SeverityType":"","PhaseLife":4,"MvtSpeed":3.403,"Surface":2409.9999,"Duration":11700,"CoolingRate":4.14}},{"geometry":{"type":"Polygon","coordinates":[[[38.326,5.994],[38.445,5.996],[38.739,6.142],[38.798,6.315],[38.804,6.372],[38.556,6.655],[38.403,6.71],[38.327,6.737],[38.129,6.735],[38.047,6.705],[38.004,6.676],[37.976,6.418],[38.03,6.19],[38.14,6.106],[38.326,5.994]]]},"type":"Feature","properties":{"NumIdBirth":21,"CType":9,"LonTrajCellCG":[38.3334,38.23,38.35,38.39,38.44,38.46,38.52],"CRainRate":41.1,"ConvTypeMethod":1,"ConvType":2,"SeverityIntensity":1,"ConvTypeQuality":1,"MvtDirection":159,"CTPressure":10000.0,"NumIdCell":12,"CTReff":"","LonG":38.3334,"LatTrajCellCG":[6.383,6.52,6.5,6.58,6.61,6.66,6.69],"ExpansionRate":30.744,"CTPhase":"","LatG":6.383,"BTmin":187.18,"BTmoy":206.35,"CTCot":"","CTCwp":"","NbPosLightning":0,"SeverityType":"","PhaseLife":2,"MvtSpeed":9.132,"Surface":4195.0001,"Duration":5400,"CoolingRate":-23.4936}},{"geometry":{"type":"Polygon","coordinates":[[[37.882,5.476],[38.039,5.477],[38.097,5.678],[38.105,5.763],[38.032,5.819],[37.835,5.817],[37.754,5.788],[37.712,5.759],[37.623,5.644],[37.615,5.558],[37.652,5.53],[37.882,5.476]]]},"type":"Feature","properties":{"NumIdBirth":0,"CType":8,"LonTrajCellCG":[37.8819],"CRainRate":8.4,"ConvTypeMethod":1,"ConvType":2,"SeverityIntensity":1,"ConvTypeQuality":3,"MvtDirection":240,"CTPressure":17800.0,"NumIdCell":20,"CTReff":"","LonG":37.8819,"LatTrajCellCG":[5.6603],"ExpansionRate":"","CTPhase":"","LatG":5.6603,"BTmin":219.57,"BTmoy":242.32,"CTCot":"","CTCwp":"","NbPosLightning":0,"SeverityType":"","PhaseLife":1,"MvtSpeed":1.785,"Surface":860.0,"Duration":0,"CoolingRate":-45.5328}},{"geometry":{"type":"Polygon","coordinates":[[[39.303,5.349],[39.465,5.351],[39.633,5.41],[39.848,5.527],[39.857,5.613],[39.749,5.755],[39.586,5.753],[39.545,5.752],[39.461,5.722],[39.249,5.634],[39.206,5.605],[39.16,5.547],[39.155,5.49],[39.303,5.349]]]},"type":"Feature","properties":{"NumIdBirth":151,"CType":8,"LonTrajCellCG":[39.5056,39.53,39.63,39.64,39.67],"CRainRate":6.7,"ConvTypeMethod":1,"ConvType":2,"SeverityIntensity":2,"ConvTypeQuality":3,"MvtDirection":247,"CTPressure":10400.0,"NumIdCell":23,"CTReff":"","LonG":39.5056,"LatTrajCellCG":[5.5527,5.58,5.58,5.6,5.58],"ExpansionRate":124.4161,"CTPhase":"","LatG":5.5527,"BTmin":203.24,"BTmoy":242.29,"CTCot":"","CTCwp":"","NbPosLightning":0,"SeverityType":"","PhaseLife":3,"MvtSpeed":5.092,"Surface":1610.0,"Duration":3600,"CoolingRate":-70.8552}},{"geometry":{"type":"Polygon","coordinates":[[[39.913,5.356],[40.078,5.358],[40.089,5.472],[39.966,5.471],[39.925,5.47],[39.913,5.356]]]},"type":"Feature","properties":{"NumIdBirth":182,"CType":8,"LonTrajCellCG":[40.0012,40.03,40.1],"CRainRate":8.8,"ConvTypeMethod":1,"ConvType":1,"SeverityIntensity":3,"ConvTypeQuality":1,"MvtDirection":252,"CTPressure":13700.0,"NumIdCell":24,"CTReff":"","LonG":40.0012,"LatTrajCellCG":[5.4141,5.41,5.46],"ExpansionRate":"","CTPhase":"","LatG":5.4141,"BTmin":212.67,"BTmoy":227.66,"CTCot":"","CTCwp":"","NbPosLightning":0,"SeverityType":"","PhaseLife":2,"MvtSpeed":4.95,"Surface":130.0,"Duration":1800,"CoolingRate":-132.3288}},{"geometry":{"type":"Polygon","coordinates":[[[40.775,5.28],[40.984,5.282],[40.99,5.34],[40.909,5.367],[40.783,5.366],[40.775,5.28]]]},"type":"Feature","properties":{"NumIdBirth":155,"CType":8,"LonTrajCellCG":[40.8691,40.84,40.71,40.66,40.65,40.63,40.73,40.73,40.73,40.75,40.64,40.7],"CRainRate":0.0,"ConvTypeMethod":1,"ConvType":1,"SeverityIntensity":0,"ConvTypeQuality":2,"MvtDirection":118,"CTPressure":20700.0,"NumIdCell":27,"CTReff":"","LonG":40.8691,"LatTrajCellCG":[5.3192,5.34,5.21,5.19,5.2,5.22,5.31,5.32,5.34,5.35,5.23,5.26],"ExpansionRate":-252.072,"CTPhase":"","LatG":5.3192,"BTmin":227.47,"BTmoy":232.38,"CTCot":"","CTCwp":"","NbPosLightning":0,"SeverityType":"","PhaseLife":4,"MvtSpeed":2.658,"Surface":90.0,"Duration":14400,"CoolingRate":13.284}},{"geometry":{"type":"Polygon","coordinates":[[[39.659,4.81],[39.864,4.812],[39.884,5.041],[39.807,5.097],[39.728,5.125],[39.606,5.124],[39.563,5.095],[39.517,5.037],[39.512,4.98],[39.547,4.923],[39.583,4.866],[39.659,4.81]]]},"type":"Feature","properties":{"NumIdBirth":151,"CType":9,"LonTrajCellCG":[39.7152,39.87,39.97,39.96,39.95,39.98],"CRainRate":34.6,"ConvTypeMethod":1,"ConvType":2,"SeverityIntensity":0,"ConvTypeQuality":1,"MvtDirection":275,"CTPressure":10100.0,"NumIdCell":28,"CTReff":"","LonG":39.7152,"LatTrajCellCG":[4.9648,4.9,4.88,4.89,4.87,4.87],"ExpansionRate":239.8321,"CTPhase":"","LatG":4.9648,"BTmin":199.77,"BTmoy":206.54,"CTCot":"","CTCwp":"","NbPosLightning":0,"SeverityType":"","PhaseLife":4,"MvtSpeed":7.678,"Surface":805.0,"Duration":4500,"CoolingRate":15.2856}},{"geometry":{"type":"Polygon","coordinates":[[[40.063,4.243],[40.351,4.246],[40.436,4.275],[40.48,4.304],[40.501,4.561],[40.464,4.618],[40.308,4.731],[40.267,4.731],[40.226,4.73],[40.054,4.643],[40.049,4.586],[40.03,4.357],[40.024,4.271],[40.063,4.243]]]},"type":"Feature","properties":{"NumIdBirth":116,"CType":8,"LonTrajCellCG":[40.2938,40.32,40.24,40.33,40.39,40.27,40.49,40.59,40.66,40.68,40.7,40.83],"CRainRate":23.3,"ConvTypeMethod":1,"ConvType":2,"SeverityIntensity":0,"ConvTypeQuality":1,"MvtDirection":198,"CTPressure":16300.0,"NumIdCell":29,"CTReff":"","LonG":40.2938,"LatTrajCellCG":[4.4538,4.51,4.55,4.55,4.56,4.61,4.6,4.59,4.63,4.71,4.74,4.68],"ExpansionRate":-194.0399,"CTPhase":"","LatG":4.4538,"BTmin":210.28,"BTmoy":215.73,"CTCot":"","CTCwp":"","NbPosLightning":0,"SeverityType":"","PhaseLife":4,"MvtSpeed":3.478,"Surface":1344.9999,"Duration":11700,"CoolingRate":8.5032}},{"geometry":{"type":"Polygon","coordinates":[[[40.889,4.222],[41.057,4.224],[41.104,4.281],[41.108,4.338],[41.027,4.366],[40.945,4.394],[40.822,4.422],[40.738,4.421],[40.694,4.392],[40.687,4.306],[40.727,4.278],[40.889,4.222]]]},"type":"Feature","properties":{"NumIdBirth":187,"CType":8,"LonTrajCellCG":[40.8898,40.87,40.94],"CRainRate":9.4,"ConvTypeMethod":1,"ConvType":2,"SeverityIntensity":0,"ConvTypeQuality":1,"MvtDirection":233,"CTPressure":17900.0,"NumIdCell":31,"CTReff":"","LonG":40.8898,"LatTrajCellCG":[4.3167,4.35,4.37],"ExpansionRate":60.3361,"CTPhase":"","LatG":4.3167,"BTmin":221.24,"BTmoy":231.15,"CTCot":"","CTCwp":"","NbPosLightning":0,"SeverityType":"","PhaseLife":4,"MvtSpeed":4.532,"Surface":430.0,"Duration":1800,"CoolingRate":6.7608}},{"geometry":{"type":"Polygon","coordinates":[[[40.857,4.823],[40.94,4.823],[40.985,4.853],[40.993,4.939],[40.956,4.995],[40.917,5.024],[40.75,5.022],[40.708,5.021],[40.697,4.907],[40.857,4.823]]]},"type":"Feature","properties":{"NumIdBirth":148,"CType":13,"LonTrajCellCG":[40.8482,40.84],"CRainRate":0.0,"ConvTypeMethod":1,"ConvType":2,"SeverityIntensity":0,"ConvTypeQuality":3,"MvtDirection":156,"CTPressure":17000.0,"NumIdCell":32,"CTReff":"","LonG":40.8482,"LatTrajCellCG":[4.937,4.97],"ExpansionRate":-15.0479,"CTPhase":"","LatG":4.937,"BTmin":225.37,"BTmoy":237.95,"CTCot":"","CTCwp":"","NbPosLightning":0,"SeverityType":"","PhaseLife":2,"MvtSpeed":2.748,"Surface":325.0,"Duration":900,"CoolingRate":-5.9256}},{"geometry":{"type":"Polygon","coordinates":[[[39.158,4.092],[39.279,4.093],[39.481,4.095],[39.643,4.097],[39.688,4.154],[39.733,4.211],[39.742,4.326],[39.629,4.439],[39.514,4.523],[39.473,4.523],[39.432,4.522],[39.012,4.291],[39.003,4.176],[39.079,4.12],[39.158,4.092]]]},"type":"Feature","properties":{"NumIdBirth":176,"CType":8,"LonTrajCellCG":[39.4075,39.55,39.59,39.63,39.68,39.72,39.77,39.81,39.84,39.86,39.88,39.86],"CRainRate":14.3,"ConvTypeMethod":1,"ConvType":2,"SeverityIntensity":2,"ConvTypeQuality":1,"MvtDirection":222,"CTPressure":14300.0,"NumIdCell":33,"CTReff":"","LonG":39.4075,"LatTrajCellCG":[4.262,4.33,4.39,4.44,4.47,4.5,4.53,4.55,4.57,4.59,4.61,4.62],"ExpansionRate":325.5841,"CTPhase":"","LatG":4.262,"BTmin":211.88,"BTmoy":244.31,"CTCot":"","CTCwp":"","NbPosLightning":0,"SeverityType":"","PhaseLife":2,"MvtSpeed":9.218,"Surface":1824.9999,"Duration":15300,"CoolingRate":-85.8672}},{"geometry":{"type":"Polygon","coordinates":[[[38.934,4.318],[39.014,4.319],[39.058,4.376],[39.072,4.548],[39.034,4.576],[38.833,4.574],[38.822,4.431],[38.858,4.375],[38.934,4.318]]]},"type":"Feature","properties":{"NumIdBirth":136,"CType":7,"LonTrajCellCG":[38.9503,38.96,39.0,39.09],"CRainRate":0.0,"ConvTypeMethod":1,"ConvType":2,"SeverityIntensity":0,"ConvTypeQuality":3,"MvtDirection":214,"CTPressure":24300.0,"NumIdCell":34,"CTReff":"","LonG":38.9503,"LatTrajCellCG":[4.4656,4.5,4.52,4.51],"ExpansionRate":159.6241,"CTPhase":"","LatG":4.4656,"BTmin":236.75,"BTmoy":251.33,"CTCot":"","CTCwp":"","NbPosLightning":0,"SeverityType":"","PhaseLife":2,"MvtSpeed":4.412,"Surface":425.0,"Duration":2700,"CoolingRate":-8.7912}},{"geometry":{"type":"Polygon","coordinates":[[[37.483,4.93],[37.561,4.931],[37.761,4.99],[37.802,5.019],[37.807,5.076],[37.773,5.133],[37.739,5.189],[37.702,5.217],[37.315,5.242],[37.238,5.241],[37.199,5.24],[37.076,5.154],[37.071,5.097],[37.101,4.983],[37.483,4.93]]]},"type":"Feature","properties":{"NumIdBirth":145,"CType":8,"LonTrajCellCG":[37.4331,37.48,37.51,37.52],"CRainRate":26.0,"ConvTypeMethod":1,"ConvType":2,"SeverityIntensity":1,"ConvTypeQuality":3,"MvtDirection":185,"CTPressure":10000.0,"NumIdCell":57,"CTReff":"","LonG":37.4331,"LatTrajCellCG":[5.0912,5.11,5.13,5.13],"ExpansionRate":1417.752,"CTPhase":"","LatG":5.0912,"BTmin":201.29,"BTmoy":217.63,"CTCot":"","CTCwp":"","NbPosLightning":0,"SeverityType":"","PhaseLife":4,"MvtSpeed":3.011,"Surface":1530.0,"Duration":2700,"CoolingRate":4.1616}},{"geometry":{"type":"Polygon","coordinates":[[[37.338,4.56],[37.416,4.56],[37.422,4.646],[37.388,4.702],[37.319,4.815],[37.282,4.843],[36.935,4.84],[36.895,4.811],[36.89,4.754],[36.922,4.669],[37.338,4.56]]]},"type":"Feature","properties":{"NumIdBirth":148,"CType":8,"LonTrajCellCG":[37.1619,37.16,37.19,37.16,37.2,37.2,37.42],"CRainRate":0.0,"ConvTypeMethod":1,"ConvType":2,"SeverityIntensity":0,"ConvTypeQuality":3,"MvtDirection":218,"CTPressure":20500.0,"NumIdCell":59,"CTReff":"","LonG":37.1619,"LatTrajCellCG":[4.7273,4.74,4.74,4.74,4.74,4.82,4.9],"ExpansionRate":-21.744,"CTPhase":"","LatG":4.7273,"BTmin":229.5,"BTmoy":245.1,"CTCot":"","CTCwp":"","NbPosLightning":0,"SeverityType":"","PhaseLife":4,"MvtSpeed":1.985,"Surface":745.0,"Duration":5400,"CoolingRate":17.8056}},{"geometry":{"type":"Polygon","coordinates":[[[38.086,4.112],[38.165,4.112],[38.173,4.226],[38.111,4.453],[38.073,4.481],[37.997,4.509],[37.805,4.564],[37.226,4.615],[37.072,4.614],[37.061,4.472],[37.094,4.387],[37.167,4.331],[37.352,4.219],[38.086,4.112]]]},"type":"Feature","properties":{"NumIdBirth":167,"CType":8,"LonTrajCellCG":[37.6177,37.73,37.76,37.69,37.62,37.81,37.82,37.85,37.74,37.76,37.79,37.8],"CRainRate":13.1,"ConvTypeMethod":1,"ConvType":2,"SeverityIntensity":0,"ConvTypeQuality":1,"MvtDirection":243,"CTPressure":15800.0,"NumIdCell":60,"CTReff":"","LonG":37.6177,"LatTrajCellCG":[4.3917,4.41,4.42,4.44,4.51,4.46,4.47,4.48,4.67,4.72,4.73,4.66],"ExpansionRate":311.472,"CTPhase":"","LatG":4.3917,"BTmin":211.09,"BTmoy":235.82,"CTCot":"","CTCwp":"","NbPosLightning":0,"SeverityType":"","PhaseLife":3,"MvtSpeed":6.392,"Surface":3255.0001,"Duration":13500,"CoolingRate":0.144}},{"geometry":{"type":"Polygon","coordinates":[[[38.379,3.773],[38.458,3.774],[38.543,3.86],[38.55,3.973],[38.275,4.0],[38.157,3.999],[37.992,3.884],[37.988,3.827],[38.144,3.8],[38.379,3.773]]]},"type":"Feature","properties":{"NumIdBirth":184,"CType":8,"LonTrajCellCG":[38.2918,38.23,38.19,38.23,38.24,38.23,38.27],"CRainRate":3.2,"ConvTypeMethod":1,"ConvType":2,"SeverityIntensity":0,"ConvTypeQuality":1,"MvtDirection":128,"CTPressure":18900.0,"NumIdCell":61,"CTReff":"","LonG":38.2918,"LatTrajCellCG":[3.8878,3.91,3.95,3.98,4.02,4.04,4.04],"ExpansionRate":75.96,"CTPhase":"","LatG":3.8878,"BTmin":225.37,"BTmoy":239.87,"CTCot":"","CTCwp":"","NbPosLightning":0,"SeverityType":"","PhaseLife":3,"MvtSpeed":4.399,"Surface":640.0,"Duration":5400,"CoolingRate":2.4696}},{"geometry":{"type":"Polygon","coordinates":[[[38.616,3.775],[38.816,3.776],[38.823,3.89],[38.787,3.947],[38.751,4.004],[38.671,4.003],[38.632,4.002],[38.548,3.945],[38.543,3.86],[38.579,3.803],[38.616,3.775]]]},"type":"Feature","properties":{"NumIdBirth":180,"CType":8,"LonTrajCellCG":[38.6865,38.65,38.74,38.76,38.78,38.8,38.85,38.89,38.92],"CRainRate":6.4,"ConvTypeMethod":1,"ConvType":2,"SeverityIntensity":2,"ConvTypeQuality":3,"MvtDirection":176,"CTPressure":18300.0,"NumIdCell":62,"CTReff":"","LonG":38.6865,"LatTrajCellCG":[3.8793,3.94,3.98,4.04,4.06,4.08,4.08,4.1,4.1],"ExpansionRate":447.6241,"CTPhase":"","LatG":3.8793,"BTmin":223.5,"BTmoy":238.35,"CTCot":"","CTCwp":"","NbPosLightning":0,"SeverityType":"","PhaseLife":2,"MvtSpeed":4.491,"Surface":405.0,"Duration":7200,"CoolingRate":-49.7952}},{"geometry":{"type":"Polygon","coordinates":[[[40.906,5.74],[41.074,5.742],[41.119,5.772],[41.135,5.915],[41.015,5.971],[40.889,5.97],[40.844,5.94],[40.831,5.825],[40.867,5.768],[40.906,5.74]]]},"type":"Feature","properties":{"NumIdBirth":130,"CType":7,"LonTrajCellCG":[40.9819,41.04,41.1,41.15,41.24,41.3,41.33],"CRainRate":0.0,"ConvTypeMethod":1,"ConvType":2,"SeverityIntensity":0,"ConvTypeQuality":3,"MvtDirection":250,"CTPressure":24600.0,"NumIdCell":70,"CTReff":"","LonG":40.9819,"LatTrajCellCG":[5.8522,5.86,5.88,5.9,5.87,5.9,5.89],"ExpansionRate":153.648,"CTPhase":"","LatG":5.8522,"BTmin":247.5,"BTmoy":260.32,"CTCot":"","CTCwp":"","NbPosLightning":0,"SeverityType":"","PhaseLife":4,"MvtSpeed":6.35,"Surface":465.0,"Duration":5400,"CoolingRate":8.6112}},{"geometry":{"type":"Polygon","coordinates":[[[40.559,6.712],[40.643,6.713],[40.772,6.744],[40.806,7.003],[40.729,7.06],[40.478,7.056],[40.142,7.022],[40.124,6.878],[40.192,6.764],[40.559,6.712]]]},"type":"Feature","properties":{"NumIdBirth":37,"CType":14,"LonTrajCellCG":[40.5097,40.65,40.67,40.67,40.54],"CRainRate":4.0,"ConvTypeMethod":1,"ConvType":2,"SeverityIntensity":1,"ConvTypeQuality":2,"MvtDirection":278,"CTPressure":15200.0,"NumIdCell":74,"CTReff":"","LonG":40.5097,"LatTrajCellCG":[6.8939,6.9,6.95,6.93,6.94],"ExpansionRate":493.1282,"CTPhase":"","LatG":6.8939,"BTmin":221.9,"BTmoy":258.28,"CTCot":"","CTCwp":"","NbPosLightning":0,"SeverityType":"","PhaseLife":2,"MvtSpeed":2.673,"Surface":1364.9999,"Duration":3600,"CoolingRate":-13.8168}},{"geometry":{"type":"Polygon","coordinates":[[[32.661,4.293],[32.91,4.295],[32.985,4.352],[33.006,4.69],[32.65,4.687],[32.613,4.659],[32.606,4.546],[32.631,4.377],[32.661,4.293]]]},"type":"Feature","properties":{"NumIdBirth":112,"CType":9,"LonTrajCellCG":[32.8091,32.82,32.83,32.85,32.84,32.84,32.92,32.93],"CRainRate":27.0,"ConvTypeMethod":1,"ConvType":2,"SeverityIntensity":0,"ConvTypeQuality":1,"MvtDirection":207,"CTPressure":10700.0,"NumIdCell":106,"CTReff":"","LonG":32.8091,"LatTrajCellCG":[4.5036,4.53,4.55,4.55,4.55,4.54,4.56,4.56],"ExpansionRate":80.136,"CTPhase":"","LatG":4.5036,"BTmin":198.73,"BTmoy":210.95,"CTCot":"","CTCwp":"","NbPosLightning":0,"SeverityType":"","PhaseLife":2,"MvtSpeed":1.879,"Surface":1384.9999,"Duration":6300,"CoolingRate":-2.088}},{"geometry":{"type":"Polygon","coordinates":[[[32.802,3.619],[32.873,3.62],[32.984,3.705],[33.182,4.072],[33.194,4.269],[32.981,4.296],[32.803,4.294],[32.729,4.237],[32.686,4.096],[32.64,3.899],[32.634,3.787],[32.734,3.675],[32.802,3.619]]]},"type":"Feature","properties":{"NumIdBirth":167,"CType":8,"LonTrajCellCG":[32.8963,32.89,32.88,32.88,32.9,32.91,32.94,32.97,32.98,32.99,32.91,32.8],"CRainRate":16.8,"ConvTypeMethod":1,"ConvType":2,"SeverityIntensity":0,"ConvTypeQuality":1,"MvtDirection":164,"CTPressure":12800.0,"NumIdCell":107,"CTReff":"","LonG":32.8963,"LatTrajCellCG":[4.0017,4.04,4.07,4.1,4.19,4.21,4.23,4.22,4.22,4.2,4.14,4.02],"ExpansionRate":-89.28,"CTPhase":"","LatG":4.0017,"BTmin":206.0,"BTmoy":216.54,"CTCot":"","CTCwp":"","NbPosLightning":0,"SeverityType":"","PhaseLife":4,"MvtSpeed":3.229,"Surface":2395.0001,"Duration":20700,"CoolingRate":23.0184}},{"geometry":{"type":"Polygon","coordinates":[[[32.288,3.925],[32.394,3.925],[32.517,4.236],[32.521,4.32],[32.489,4.376],[32.388,4.46],[32.216,4.514],[31.8,4.595],[31.73,4.595],[31.723,4.482],[31.776,4.174],[32.079,3.951],[32.288,3.925]]]},"type":"Feature","properties":{"NumIdBirth":190,"CType":9,"LonTrajCellCG":[32.1282,32.15,32.19,32.21,32.25,32.3,32.38,32.63,32.63,32.58,32.63,32.65],"CRainRate":9.4,"ConvTypeMethod":1,"ConvType":2,"SeverityIntensity":0,"ConvTypeQuality":1,"MvtDirection":239,"CTPressure":12200.0,"NumIdCell":108,"CTReff":"","LonG":32.1282,"LatTrajCellCG":[4.2602,4.28,4.29,4.31,4.32,4.32,4.3,4.26,4.26,4.21,4.21,4.21],"ExpansionRate":-16.3439,"CTPhase":"","LatG":4.2602,"BTmin":202.76,"BTmoy":216.5,"CTCot":"","CTCwp":"","NbPosLightning":0,"SeverityType":"","PhaseLife":4,"MvtSpeed":4.679,"Surface":3644.9999,"Duration":11700,"CoolingRate":6.0336}},{"geometry":{"type":"Polygon","coordinates":[[[31.86,3.781],[31.965,3.782],[32.073,3.839],[32.077,3.923],[31.943,4.035],[31.803,4.034],[31.793,3.837],[31.86,3.781]]]},"type":"Feature","properties":{"NumIdBirth":96,"CType":8,"LonTrajCellCG":[31.9152,31.94,31.92,31.98,32.08],"CRainRate":16.0,"ConvTypeMethod":1,"ConvType":2,"SeverityIntensity":0,"ConvTypeQuality":2,"MvtDirection":265,"CTPressure":10700.0,"NumIdCell":109,"CTReff":"","LonG":31.9152,"LatTrajCellCG":[3.908,3.9,3.88,3.86,3.91],"ExpansionRate":238.4641,"CTPhase":"","LatG":3.908,"BTmin":206.89,"BTmoy":217.17,"CTCot":"","CTCwp":"","NbPosLightning":0,"SeverityType":"","PhaseLife":4,"MvtSpeed":3.32,"Surface":460.0,"Duration":3600,"CoolingRate":10.9584}},{"geometry":{"type":"Polygon","coordinates":[[[31.946,3.389],[32.192,3.391],[32.404,3.42],[32.441,3.448],[32.443,3.504],[32.385,3.757],[32.353,3.813],[32.288,3.925],[32.182,3.924],[32.074,3.867],[31.821,3.697],[31.816,3.585],[31.847,3.501],[31.913,3.417],[31.946,3.389]]]},"type":"Feature","properties":{"NumIdBirth":97,"CType":9,"LonTrajCellCG":[32.1375,32.32,32.29,32.3,32.21,32.21,32.24,32.17,32.23],"CRainRate":24.1,"ConvTypeMethod":1,"ConvType":2,"SeverityIntensity":0,"ConvTypeQuality":1,"MvtDirection":201,"CTPressure":10000.0,"NumIdCell":110,"CTReff":"","LonG":32.1375,"LatTrajCellCG":[3.6193,3.57,3.54,3.52,3.62,3.62,3.63,3.74,3.78],"ExpansionRate":"","CTPhase":"","LatG":3.6193,"BTmin":198.2,"BTmoy":214.99,"CTCot":"","CTCwp":"","NbPosLightning":0,"SeverityType":"","PhaseLife":3,"MvtSpeed":0.748,"Surface":2264.9999,"Duration":7200,"CoolingRate":2.1888}},{"geometry":{"type":"Polygon","coordinates":[[[32.578,3.365],[32.684,3.365],[32.726,3.506],[32.731,3.619],[32.662,3.646],[32.45,3.645],[32.413,3.617],[32.409,3.532],[32.578,3.365]]]},"type":"Feature","properties":{"NumIdBirth":155,"CType":8,"LonTrajCellCG":[32.5816,32.62,32.65,32.78,32.81,32.93,32.95,32.96,33.01,33.11,33.21,33.33],"CRainRate":21.4,"ConvTypeMethod":1,"ConvType":2,"SeverityIntensity":3,"ConvTypeQuality":1,"MvtDirection":242,"CTPressure":11200.0,"NumIdCell":111,"CTReff":"","LonG":32.5816,"LatTrajCellCG":[3.5423,3.55,3.55,3.63,3.63,3.71,3.71,3.73,3.74,3.75,3.77,3.77],"ExpansionRate":969.984,"CTPhase":"","LatG":3.5423,"BTmin":207.33,"BTmoy":229.44,"CTCot":"","CTCwp":"","NbPosLightning":0,"SeverityType":"","PhaseLife":3,"MvtSpeed":6.887,"Surface":465.0,"Duration":10800,"CoolingRate":-50.4936}},{"geometry":{"type":"Polygon","coordinates":[[[33.269,3.679],[33.412,3.68],[33.449,3.708],[33.457,3.849],[33.313,3.848],[33.237,3.763],[33.234,3.706],[33.269,3.679]]]},"type":"Feature","properties":{"NumIdBirth":101,"CType":7,"LonTrajCellCG":[33.3604,33.48,33.55,33.61,33.59],"CRainRate":1.8,"ConvTypeMethod":1,"ConvType":1,"SeverityIntensity":3,"ConvTypeQuality":3,"MvtDirection":293,"CTPressure":25100.0,"NumIdCell":116,"CTReff":"","LonG":33.3604,"LatTrajCellCG":[3.7605,3.65,3.67,3.68,3.63],"ExpansionRate":1649.4482,"CTPhase":"","LatG":3.7605,"BTmin":237.76,"BTmoy":253.59,"CTCot":"","CTCwp":"","NbPosLightning":0,"SeverityType":"","PhaseLife":2,"MvtSpeed":10.692,"Surface":225.0,"Duration":3600,"CoolingRate":-79.56}},{"geometry":{"type":"Polygon","coordinates":[[[32.792,2.524],[32.934,2.524],[33.006,2.553],[33.011,2.693],[32.904,2.693],[32.794,2.58],[32.792,2.524]]]},"type":"Feature","properties":{"NumIdBirth":251,"CType":6,"LonTrajCellCG":[32.9215,32.96,33.06,33.11],"CRainRate":0.0,"ConvTypeMethod":1,"ConvType":2,"SeverityIntensity":0,"ConvTypeQuality":3,"MvtDirection":250,"CTPressure":20500.0,"NumIdCell":120,"CTReff":"","LonG":32.9215,"LatTrajCellCG":[2.6005,2.58,2.62,2.64],"ExpansionRate":-38.9519,"CTPhase":"","LatG":2.6005,"BTmin":253.92,"BTmoy":263.59,"CTCot":"","CTCwp":"","NbPosLightning":0,"SeverityType":"","PhaseLife":2,"MvtSpeed":8.261,"Surface":170.0,"Duration":2700,"CoolingRate":-7.704}},{"geometry":{"type":"Polygon","coordinates":[[[32.976,2.693],[33.154,2.694],[33.37,2.751],[33.444,2.808],[33.448,2.892],[33.242,3.116],[33.171,3.116],[33.02,2.918],[32.981,2.833],[32.976,2.693]]]},"type":"Feature","properties":{"NumIdBirth":247,"CType":7,"LonTrajCellCG":[33.1893,33.25,33.25],"CRainRate":0.0,"ConvTypeMethod":1,"ConvType":1,"SeverityIntensity":1,"ConvTypeQuality":3,"MvtDirection":240,"CTPressure":23000.0,"NumIdCell":122,"CTReff":"","LonG":33.1893,"LatTrajCellCG":[2.8448,2.82,2.83],"ExpansionRate":276.9121,"CTPhase":"","LatG":2.8448,"BTmin":245.97,"BTmoy":263.82,"CTCot":"","CTCwp":"","NbPosLightning":0,"SeverityType":"","PhaseLife":2,"MvtSpeed":4.306,"Surface":890.0,"Duration":1800,"CoolingRate":-20.3112}},{"geometry":{"type":"Polygon","coordinates":[[[30.07,1.73],[30.205,1.73],[30.24,1.758],[30.415,1.982],[30.451,2.066],[30.487,2.15],[30.49,2.262],[30.458,2.317],[30.359,2.429],[30.157,2.484],[29.852,2.483],[29.785,2.482],[29.716,2.426],[29.68,2.37],[29.674,2.147],[29.767,1.812],[30.07,1.73]]]},"type":"Feature","properties":{"NumIdBirth":101,"CType":9,"LonTrajCellCG":[30.0586,30.09,30.31,30.09,30.28,30.26,30.3,30.31,30.33,30.36,30.39,30.42],"CRainRate":45.4,"ConvTypeMethod":1,"ConvType":2,"SeverityIntensity":0,"ConvTypeQuality":2,"MvtDirection":287,"CTPressure":7300.0,"NumIdCell":131,"CTReff":"","LonG":30.0586,"LatTrajCellCG":[2.1389,2.13,2.07,2.01,2.08,2.03,2.01,1.96,1.94,1.91,1.89,1.87],"ExpansionRate":82.008,"CTPhase":"","LatG":2.1389,"BTmin":197.66,"BTmoy":216.74,"CTCot":"","CTCwp":"","NbPosLightning":0,"SeverityType":"","PhaseLife":4,"MvtSpeed":6.113,"Surface":5070.0001,"Duration":18900,"CoolingRate":4.4352}},{"geometry":{"type":"Polygon","coordinates":[[[29.381,0.724],[29.583,0.725],[29.65,0.78],[29.86,1.366],[29.896,1.478],[29.897,1.534],[29.864,1.59],[29.697,1.673],[29.429,1.7],[29.362,1.7],[29.221,1.309],[29.185,1.142],[29.184,1.058],[29.216,0.919],[29.248,0.808],[29.381,0.724]]]},"type":"Feature","properties":{"NumIdBirth":283,"CType":9,"LonTrajCellCG":[29.5194,29.52,29.57,29.61,29.64,29.69,29.72,29.76,29.76,29.79],"CRainRate":38.2,"ConvTypeMethod":1,"ConvType":2,"SeverityIntensity":0,"ConvTypeQuality":1,"MvtDirection":337,"CTPressure":7000.0,"NumIdCell":133,"CTReff":"","LonG":29.5194,"LatTrajCellCG":[1.2202,0.99,1.0,1.0,1.01,1.02,1.02,1.02,1.01,1.01],"ExpansionRate":273.3121,"CTPhase":"","LatG":1.2202,"BTmin":197.66,"BTmoy":216.21,"CTCot":"","CTCwp":"","NbPosLightning":0,"SeverityType":"","PhaseLife":3,"MvtSpeed":14.285,"Surface":4725.0002,"Duration":8100,"CoolingRate":-4.3632}},{"geometry":{"type":"Polygon","coordinates":[[[29.717,0.669],[29.885,0.669],[29.919,0.725],[29.921,0.864],[29.887,0.892],[29.82,0.92],[29.652,0.92],[29.651,0.864],[29.683,0.697],[29.717,0.669]]]},"type":"Feature","properties":{"NumIdBirth":273,"CType":9,"LonTrajCellCG":[29.7915,29.85,29.91,29.96,30.0,29.99],"CRainRate":32.8,"ConvTypeMethod":1,"ConvType":2,"SeverityIntensity":1,"ConvTypeQuality":1,"MvtDirection":312,"CTPressure":7800.0,"NumIdCell":134,"CTReff":"","LonG":29.7915,"LatTrajCellCG":[0.7946,0.73,0.63,0.6,0.6,0.58],"ExpansionRate":671.1841,"CTPhase":"","LatG":0.7946,"BTmin":198.73,"BTmoy":212.99,"CTCot":"","CTCwp":"","NbPosLightning":0,"SeverityType":"","PhaseLife":4,"MvtSpeed":7.426,"Surface":490.0,"Duration":4500,"CoolingRate":20.3112}},{"geometry":{"type":"Polygon","coordinates":[[[28.812,0.167],[28.878,0.167],[28.945,0.195],[29.045,0.278],[29.045,0.39],[29.012,0.418],[28.912,0.445],[28.746,0.445],[28.713,0.417],[28.712,0.278],[28.745,0.195],[28.812,0.167]]]},"type":"Feature","properties":{"NumIdBirth":339,"CType":9,"LonTrajCellCG":[28.8677,28.92,28.95,28.98,29.02,29.03,28.97,29.02,29.09,29.12,29.2,29.24],"CRainRate":27.9,"ConvTypeMethod":1,"ConvType":2,"SeverityIntensity":0,"ConvTypeQuality":1,"MvtDirection":291,"CTPressure":7100.0,"NumIdCell":135,"CTReff":"","LonG":28.8677,"LatTrajCellCG":[0.3201,0.31,0.28,0.25,0.22,0.22,0.12,0.12,0.05,0.08,0.09,0.11],"ExpansionRate":38.3041,"CTPhase":"","LatG":0.3201,"BTmin":203.24,"BTmoy":207.47,"CTCot":"","CTCwp":"","NbPosLightning":0,"SeverityType":"","PhaseLife":4,"MvtSpeed":5.99,"Surface":665.0,"Duration":10800,"CoolingRate":15.948}},{"geometry":{"type":"Polygon","coordinates":[[[29.078,0.223],[29.145,0.223],[29.345,0.251],[29.346,0.39],[29.245,0.418],[29.112,0.418],[29.045,0.362],[29.045,0.251],[29.078,0.223]]]},"type":"Feature","properties":{"NumIdBirth":276,"CType":9,"LonTrajCellCG":[29.1902,29.28,29.31,29.33,29.35,29.36],"CRainRate":28.0,"ConvTypeMethod":1,"ConvType":2,"SeverityIntensity":3,"ConvTypeQuality":1,"MvtDirection":260,"CTPressure":8500.0,"NumIdCell":136,"CTReff":"","LonG":29.1902,"LatTrajCellCG":[0.327,0.36,0.34,0.32,0.28,0.25],"ExpansionRate":549.5761,"CTPhase":"","LatG":0.327,"BTmin":196.0,"BTmoy":206.06,"CTCot":"","CTCwp":"","NbPosLightning":0,"SeverityType":"","PhaseLife":3,"MvtSpeed":7.066,"Surface":355.0,"Duration":4500,"CoolingRate":-53.7984}},{"geometry":{"type":"Polygon","coordinates":[[[29.479,0.167],[29.546,0.167],[29.546,0.307],[29.513,0.334],[29.446,0.362],[29.379,0.362],[29.379,0.223],[29.479,0.167]]]},"type":"Feature","properties":{"NumIdBirth":275,"CType":9,"LonTrajCellCG":[29.4635,29.49,29.52,29.56,29.56,29.57],"CRainRate":13.5,"ConvTypeMethod":1,"ConvType":2,"SeverityIntensity":0,"ConvTypeQuality":3,"MvtDirection":300,"CTPressure":11400.0,"NumIdCell":137,"CTReff":"","LonG":29.4635,"LatTrajCellCG":[0.2675,0.25,0.22,0.16,0.15,0.12],"ExpansionRate":-21.456,"CTPhase":"","LatG":0.2675,"BTmin":209.45,"BTmoy":214.16,"CTCot":"","CTCwp":"","NbPosLightning":0,"SeverityType":"","PhaseLife":4,"MvtSpeed":5.577,"Surface":175.0,"Duration":4500,"CoolingRate":8.4096}},{"geometry":{"type":"Polygon","coordinates":[[[29.044,-0.167],[29.178,-0.167],[29.245,-0.111],[29.278,-0.056],[29.278,-0.0],[29.044,-0.0],[29.044,-0.167]]]},"type":"Feature","properties":{"NumIdBirth":287,"CType":7,"LonTrajCellCG":[29.1414,29.16,29.21,28.94,28.98,29.01,29.1,29.13,29.06,28.96,29.0,29.07],"CRainRate":3.2,"ConvTypeMethod":1,"ConvType":1,"SeverityIntensity":3,"ConvTypeQuality":1,"MvtDirection":287,"CTPressure":18100.0,"NumIdCell":138,"CTReff":"","LonG":29.1414,"LatTrajCellCG":[-0.0751,-0.1,-0.12,-0.27,-0.24,-0.22,-0.24,-0.23,-0.29,-0.41,-0.36,-0.27],"ExpansionRate":973.728,"CTPhase":"","LatG":-0.0751,"BTmin":230.07,"BTmoy":252.84,"CTCot":"","CTCwp":"","NbPosLightning":0,"SeverityType":"","PhaseLife":3,"MvtSpeed":4.271,"Surface":265.0,"Duration":16200,"CoolingRate":-96.3216}},{"geometry":{"type":"Polygon","coordinates":[[[28.547,-0.278],[28.646,-0.278],[28.712,-0.223],[28.712,-0.139],[28.679,-0.111],[28.513,-0.111],[28.48,-0.139],[28.481,-0.25],[28.547,-0.278]]]},"type":"Feature","properties":{"NumIdBirth":159,"CType":8,"LonTrajCellCG":[28.5927,28.64,28.85],"CRainRate":11.2,"ConvTypeMethod":1,"ConvType":2,"SeverityIntensity":1,"ConvTypeQuality":1,"MvtDirection":298,"CTPressure":12900.0,"NumIdCell":142,"CTReff":"","LonG":28.5927,"LatTrajCellCG":[-0.1887,-0.21,-0.29],"ExpansionRate":926.64,"CTPhase":"","LatG":-0.1887,"BTmin":216.77,"BTmoy":227.36,"CTCot":"","CTCwp":"","NbPosLightning":0,"SeverityType":"","PhaseLife":3,"MvtSpeed":10.779,"Surface":260.0,"Duration":1800,"CoolingRate":-15.1056}},{"geometry":{"type":"Polygon","coordinates":[[[28.482,-0.529],[28.548,-0.473],[28.58,-0.417],[28.58,-0.278],[28.514,-0.25],[28.382,-0.25],[28.316,-0.306],[28.316,-0.389],[28.35,-0.528],[28.482,-0.529]]]},"type":"Feature","properties":{"NumIdBirth":357,"CType":8,"LonTrajCellCG":[28.4489,28.49,28.54,28.58,28.62,28.66,28.71,28.71,28.78],"CRainRate":29.7,"ConvTypeMethod":1,"ConvType":2,"SeverityIntensity":2,"ConvTypeQuality":1,"MvtDirection":312,"CTPressure":7900.0,"NumIdCell":143,"CTReff":"","LonG":28.4489,"LatTrajCellCG":[-0.3821,-0.4,-0.43,-0.46,-0.48,-0.56,-0.54,-0.6,-0.57],"ExpansionRate":1191.8161,"CTPhase":"","LatG":-0.3821,"BTmin":197.66,"BTmoy":216.02,"CTCot":"","CTCwp":"","NbPosLightning":0,"SeverityType":"","PhaseLife":3,"MvtSpeed":6.018,"Surface":520.0,"Duration":7200,"CoolingRate":-40.3632}},{"geometry":{"type":"Polygon","coordinates":[[[28.221,-0.862],[28.353,-0.862],[28.451,-0.779],[28.449,-0.584],[28.317,-0.473],[28.218,-0.473],[28.185,-0.5],[28.153,-0.639],[28.154,-0.806],[28.221,-0.862]]]},"type":"Feature","properties":{"NumIdBirth":354,"CType":9,"LonTrajCellCG":[28.2978,28.34,28.39,28.44,28.49,28.51,28.55,28.57,28.6,28.7],"CRainRate":28.0,"ConvTypeMethod":1,"ConvType":2,"SeverityIntensity":1,"ConvTypeQuality":1,"MvtDirection":286,"CTPressure":7900.0,"NumIdCell":144,"CTReff":"","LonG":28.2978,"LatTrajCellCG":[-0.6774,-0.66,-0.71,-0.74,-0.74,-0.77,-0.82,-0.83,-0.84,-0.82],"ExpansionRate":706.1041,"CTPhase":"","LatG":-0.6774,"BTmin":196.0,"BTmoy":213.42,"CTCot":"","CTCwp":"","NbPosLightning":0,"SeverityType":"","PhaseLife":3,"MvtSpeed":5.613,"Surface":885.0,"Duration":8100,"CoolingRate":-8.604}},{"geometry":{"type":"Polygon","coordinates":[[[31.897,1.987],[32.072,1.988],[32.142,2.016],[32.178,2.044],[32.181,2.156],[32.113,2.212],[32.044,2.24],[31.939,2.24],[31.902,2.183],[31.897,1.987]]]},"type":"Feature","properties":{"NumIdBirth":128,"CType":8,"LonTrajCellCG":[32.0304,32.04,32.13],"CRainRate":4.2,"ConvTypeMethod":1,"ConvType":1,"SeverityIntensity":1,"ConvTypeQuality":1,"MvtDirection":281,"CTPressure":16100.0,"NumIdCell":147,"CTReff":"","LonG":32.0304,"LatTrajCellCG":[2.1074,2.11,2.08],"ExpansionRate":184.824,"CTPhase":"","LatG":2.1074,"BTmin":221.57,"BTmoy":245.29,"CTCot":"","CTCwp":"","NbPosLightning":0,"SeverityType":"","PhaseLife":2,"MvtSpeed":4.937,"Surface":495.0,"Duration":1800,"CoolingRate":-47.124}},{"geometry":{"type":"Polygon","coordinates":[[[31.401,1.566],[31.505,1.566],[31.679,1.595],[31.82,1.679],[31.855,1.707],[31.858,1.819],[31.824,1.875],[31.689,2.015],[31.584,2.014],[31.335,1.734],[31.332,1.594],[31.401,1.566]]]},"type":"Feature","properties":{"NumIdBirth":229,"CType":8,"LonTrajCellCG":[31.6024,31.61,31.63,31.67,31.69,31.71,31.76,31.8,31.82,31.84,32.12,32.03],"CRainRate":7.6,"ConvTypeMethod":1,"ConvType":2,"SeverityIntensity":0,"ConvTypeQuality":1,"MvtDirection":240,"CTPressure":15800.0,"NumIdCell":148,"CTReff":"","LonG":31.6024,"LatTrajCellCG":[1.7608,1.77,1.77,1.78,1.78,1.79,1.82,1.82,1.81,1.8,1.77,1.8],"ExpansionRate":-35.64,"CTPhase":"","LatG":1.7608,"BTmin":212.28,"BTmoy":221.88,"CTCot":"","CTCwp":"","NbPosLightning":0,"SeverityType":"","PhaseLife":4,"MvtSpeed":2.478,"Surface":1330.0,"Duration":14400,"CoolingRate":11.304}},{"geometry":{"type":"Polygon","coordinates":[[[30.774,1.145],[30.843,1.145],[30.98,1.173],[31.119,1.23],[31.362,1.37],[31.365,1.482],[31.263,1.565],[31.194,1.593],[31.125,1.593],[31.055,1.537],[30.776,1.257],[30.774,1.145]]]},"type":"Feature","properties":{"NumIdBirth":336,"CType":8,"LonTrajCellCG":[31.1088,31.22,31.19,31.19,31.2,31.2,31.2,31.15,31.18],"CRainRate":11.2,"ConvTypeMethod":1,"ConvType":2,"SeverityIntensity":0,"ConvTypeQuality":1,"MvtDirection":295,"CTPressure":11600.0,"NumIdCell":149,"CTReff":"","LonG":31.1088,"LatTrajCellCG":[1.377,1.48,1.48,1.46,1.44,1.41,1.39,1.36,1.35],"ExpansionRate":321.48,"CTPhase":"","LatG":1.377,"BTmin":209.45,"BTmoy":220.19,"CTCot":"","CTCwp":"","NbPosLightning":0,"SeverityType":"","PhaseLife":3,"MvtSpeed":2.27,"Surface":785.0,"Duration":7200,"CoolingRate":1.6056}},{"geometry":{"type":"Polygon","coordinates":[[[30.6,0.921],[30.943,0.922],[30.944,1.034],[30.91,1.062],[30.808,1.145],[30.637,1.145],[30.569,1.117],[30.566,0.949],[30.6,0.921]]]},"type":"Feature","properties":{"NumIdBirth":315,"CType":8,"LonTrajCellCG":[30.7401,30.77,30.77,30.71,30.72,30.64,30.57,30.63,30.58,30.63,30.65,30.61],"CRainRate":17.8,"ConvTypeMethod":1,"ConvType":2,"SeverityIntensity":0,"ConvTypeQuality":1,"MvtDirection":337,"CTPressure":10200.0,"NumIdCell":152,"CTReff":"","LonG":30.7401,"LatTrajCellCG":[1.0213,1.0,0.96,0.85,0.83,0.85,0.82,0.82,0.9,0.91,0.91,0.9],"ExpansionRate":104.5441,"CTPhase":"","LatG":1.0213,"BTmin":208.61,"BTmoy":216.82,"CTCot":"","CTCwp":"","NbPosLightning":0,"SeverityType":"","PhaseLife":4,"MvtSpeed":3.767,"Surface":660.0,"Duration":11700,"CoolingRate":14.256}},{"geometry":{"type":"Polygon","coordinates":[[[29.888,3.488],[29.956,3.489],[30.025,3.517],[30.205,3.742],[30.208,3.826],[30.177,3.882],[30.041,3.881],[29.863,3.684],[29.856,3.516],[29.888,3.488]]]},"type":"Feature","properties":{"NumIdBirth":177,"CType":6,"LonTrajCellCG":[30.0203,30.05,30.08,30.11],"CRainRate":0.0,"ConvTypeMethod":1,"ConvType":2,"SeverityIntensity":0,"ConvTypeQuality":1,"MvtDirection":280,"CTPressure":21200.0,"NumIdCell":186,"CTReff":"","LonG":30.0203,"LatTrajCellCG":[3.6979,3.69,3.69,3.67],"ExpansionRate":81.576,"CTPhase":"","LatG":3.6979,"BTmin":231.74,"BTmoy":261.94,"CTCot":"","CTCwp":"","NbPosLightning":0,"SeverityType":"","PhaseLife":3,"MvtSpeed":2.792,"Surface":560.0,"Duration":2700,"CoolingRate":-0.036}},{"geometry":{"type":"Polygon","coordinates":[[[30.086,3.35],[30.154,3.35],[30.432,3.492],[30.467,3.52],[30.505,3.604],[30.511,3.744],[30.446,3.8],[30.309,3.799],[30.099,3.658],[30.062,3.601],[30.055,3.434],[30.086,3.35]]]},"type":"Feature","properties":{"NumIdBirth":193,"CType":8,"LonTrajCellCG":[30.2834,30.34,30.38,30.42,30.44],"CRainRate":11.0,"ConvTypeMethod":1,"ConvType":2,"SeverityIntensity":0,"ConvTypeQuality":2,"MvtDirection":277,"CTPressure":18600.0,"NumIdCell":187,"CTReff":"","LonG":30.2834,"LatTrajCellCG":[3.6008,3.61,3.6,3.6,3.62],"ExpansionRate":297.3601,"CTPhase":"","LatG":3.6008,"BTmin":222.22,"BTmoy":252.79,"CTCot":"","CTCwp":"","NbPosLightning":0,"SeverityType":"","PhaseLife":2,"MvtSpeed":3.861,"Surface":1150.0,"Duration":3600,"CoolingRate":-2.52}},{"geometry":{"type":"Polygon","coordinates":[[[41.48,3.026],[41.608,3.027],[41.62,3.227],[41.544,3.369],[41.505,3.426],[41.382,3.483],[41.299,3.511],[41.173,3.51],[41.131,3.509],[40.997,3.394],[40.994,3.337],[41.23,3.081],[41.271,3.053],[41.48,3.026]]]},"type":"Feature","properties":{"NumIdBirth":228,"CType":14,"LonTrajCellCG":[41.3524,41.54,41.57,41.58,41.5,41.53,41.58,41.62],"CRainRate":5.4,"ConvTypeMethod":1,"ConvType":2,"SeverityIntensity":1,"ConvTypeQuality":1,"MvtDirection":234,"CTPressure":18500.0,"NumIdCell":193,"CTReff":"","LonG":41.3524,"LatTrajCellCG":[3.269,3.17,3.19,3.21,3.3,3.34,3.36,3.48],"ExpansionRate":1840.68,"CTPhase":"","LatG":3.269,"BTmin":225.67,"BTmoy":261.93,"CTCot":"","CTCwp":"","NbPosLightning":0,"SeverityType":"","PhaseLife":3,"MvtSpeed":5.866,"Surface":1700.0,"Duration":6300,"CoolingRate":-5.904}},{"geometry":{"type":"Polygon","coordinates":[[[33.887,2.079],[33.995,2.079],[34.368,2.362],[34.406,2.419],[34.409,2.503],[34.338,2.559],[34.121,2.586],[33.904,2.585],[33.794,2.557],[33.72,2.5],[33.717,2.416],[33.708,2.135],[33.743,2.107],[33.887,2.079]]]},"type":"Feature","properties":{"NumIdBirth":242,"CType":7,"LonTrajCellCG":[34.0104,34.25,34.07,34.12,34.26,34.32,34.37,34.37,34.35],"CRainRate":0.0,"ConvTypeMethod":1,"ConvType":1,"SeverityIntensity":2,"ConvTypeQuality":2,"MvtDirection":252,"CTPressure":20800.0,"NumIdCell":212,"CTReff":"","LonG":34.0104,"LatTrajCellCG":[2.3624,2.39,2.25,2.22,2.2,2.19,2.19,2.17,2.19],"ExpansionRate":1623.456,"CTPhase":"","LatG":2.3624,"BTmin":245.53,"BTmoy":265.93,"CTCot":"","CTCwp":"","NbPosLightning":0,"SeverityType":"","PhaseLife":2,"MvtSpeed":3.059,"Surface":1784.9999,"Duration":7200,"CoolingRate":-28.8864}},{"geometry":{"type":"Polygon","coordinates":[[[34.282,1.968],[34.355,1.968],[34.359,2.109],[34.287,2.137],[34.142,2.136],[34.105,2.108],[34.102,1.996],[34.282,1.968]]]},"type":"Feature","properties":{"NumIdBirth":246,"CType":8,"LonTrajCellCG":[34.2322,34.15,34.19,34.25,34.25,34.33,34.38,34.45,34.56,34.61,34.68,34.73],"CRainRate":2.5,"ConvTypeMethod":1,"ConvType":2,"SeverityIntensity":0,"ConvTypeQuality":3,"MvtDirection":315,"CTPressure":19600.0,"NumIdCell":220,"CTReff":"","LonG":34.2322,"LatTrajCellCG":[2.0574,1.97,1.97,1.96,1.82,1.76,1.79,1.78,1.86,1.88,1.85,1.84],"ExpansionRate":202.32,"CTPhase":"","LatG":2.0574,"BTmin":230.07,"BTmoy":238.65,"CTCot":"","CTCwp":"","NbPosLightning":0,"SeverityType":"","PhaseLife":4,"MvtSpeed":2.472,"Surface":280.0,"Duration":9900,"CoolingRate":32.8752}},{"geometry":{"type":"Polygon","coordinates":[[[34.298,1.012],[34.737,1.013],[34.884,1.069],[34.959,1.126],[35.039,1.464],[35.041,1.548],[34.57,1.828],[34.461,1.828],[34.351,1.8],[34.018,1.573],[33.907,1.461],[33.904,1.264],[33.939,1.236],[34.298,1.012]]]},"type":"Feature","properties":{"NumIdBirth":253,"CType":9,"LonTrajCellCG":[34.4966,34.54,34.5,34.48,34.61,34.49,34.45,34.41,34.36,34.34],"CRainRate":35.4,"ConvTypeMethod":1,"ConvType":2,"SeverityIntensity":0,"ConvTypeQuality":1,"MvtDirection":308,"CTPressure":10000.0,"NumIdCell":222,"CTReff":"","LonG":34.4966,"LatTrajCellCG":[1.3879,1.42,1.44,1.42,1.47,1.4,1.36,1.37,1.34,1.32],"ExpansionRate":166.68,"CTPhase":"","LatG":1.3879,"BTmin":191.21,"BTmoy":214.73,"CTCot":"","CTCwp":"","NbPosLightning":0,"SeverityType":"","PhaseLife":4,"MvtSpeed":1.111,"Surface":6780.0003,"Duration":8100,"CoolingRate":10.6344}},{"geometry":{"type":"Polygon","coordinates":[[[34.767,0.45],[34.841,0.45],[35.062,0.479],[35.063,0.619],[34.953,0.675],[34.88,0.704],[34.806,0.732],[34.696,0.731],[34.586,0.675],[34.585,0.562],[34.767,0.45]]]},"type":"Feature","properties":{"NumIdBirth":290,"CType":8,"LonTrajCellCG":[34.8258,34.74,34.85,34.86,34.88,34.91,34.92,34.96],"CRainRate":3.3,"ConvTypeMethod":1,"ConvType":2,"SeverityIntensity":0,"ConvTypeQuality":1,"MvtDirection":10,"CTPressure":10300.0,"NumIdCell":229,"CTReff":"","LonG":34.8258,"LatTrajCellCG":[0.5895,0.58,0.59,0.57,0.54,0.52,0.48,0.46],"ExpansionRate":304.5601,"CTPhase":"","LatG":0.5895,"BTmin":209.87,"BTmoy":242.3,"CTCot":"","CTCwp":"","NbPosLightning":0,"SeverityType":"","PhaseLife":4,"MvtSpeed":1.896,"Surface":790.0,"Duration":6300,"CoolingRate":13.6656}},{"geometry":{"type":"Polygon","coordinates":[[[35.097,0.225],[35.208,0.225],[35.209,0.31],[35.135,0.45],[35.025,0.45],[34.877,0.394],[34.767,0.338],[34.766,0.281],[34.84,0.253],[35.097,0.225]]]},"type":"Feature","properties":{"NumIdBirth":293,"CType":7,"LonTrajCellCG":[35.021,34.99,34.95],"CRainRate":1.0,"ConvTypeMethod":1,"ConvType":2,"SeverityIntensity":1,"ConvTypeQuality":1,"MvtDirection":44,"CTPressure":18600.0,"NumIdCell":231,"CTReff":"","LonG":35.021,"LatTrajCellCG":[0.3315,0.32,0.27],"ExpansionRate":380.592,"CTPhase":"","LatG":0.3315,"BTmin":228.35,"BTmoy":256.22,"CTCot":"","CTCwp":"","NbPosLightning":0,"SeverityType":"","PhaseLife":3,"MvtSpeed":3.587,"Surface":460.0,"Duration":1800,"CoolingRate":-26.3952}},{"geometry":{"type":"Polygon","coordinates":[[[34.182,-0.253],[34.291,-0.253],[34.619,-0.197],[34.729,-0.056],[34.729,0.028],[34.583,0.056],[34.437,0.056],[34.218,-0.028],[34.146,-0.084],[34.146,-0.197],[34.182,-0.253]]]},"type":"Feature","properties":{"NumIdBirth":291,"CType":7,"LonTrajCellCG":[34.4312,34.42,34.51],"CRainRate":11.3,"ConvTypeMethod":1,"ConvType":2,"SeverityIntensity":1,"ConvTypeQuality":1,"MvtDirection":328,"CTPressure":10000.0,"NumIdCell":234,"CTReff":"","LonG":34.4312,"LatTrajCellCG":[-0.0937,-0.09,-0.12],"ExpansionRate":807.48,"CTPhase":"","LatG":-0.0937,"BTmin":213.82,"BTmoy":251.51,"CTCot":"","CTCwp":"","NbPosLightning":0,"SeverityType":"","PhaseLife":3,"MvtSpeed":3.739,"Surface":910.0,"Duration":1800,"CoolingRate":-18.9216}},{"geometry":{"type":"Polygon","coordinates":[[[33.928,-0.084],[34.001,-0.084],[34.037,0.197],[34.037,0.281],[34.001,0.309],[33.929,0.337],[33.857,0.337],[33.712,0.197],[33.712,0.084],[33.748,-0.028],[33.928,-0.084]]]},"type":"Feature","properties":{"NumIdBirth":264,"CType":7,"LonTrajCellCG":[33.8732,33.82,33.79,33.76,33.74,33.56,33.56,33.53,33.52,33.51,33.49,33.51],"CRainRate":0.0,"ConvTypeMethod":1,"ConvType":1,"SeverityIntensity":1,"ConvTypeQuality":3,"MvtDirection":88,"CTPressure":21200.0,"NumIdCell":237,"CTReff":"","LonG":33.8732,"LatTrajCellCG":[0.1269,0.16,0.15,0.16,0.18,0.19,0.17,0.13,0.12,0.1,0.09,0.06],"ExpansionRate":542.376,"CTPhase":"","LatG":0.1269,"BTmin":248.15,"BTmoy":261.77,"CTCot":"","CTCwp":"","NbPosLightning":0,"SeverityType":"","PhaseLife":2,"MvtSpeed":4.394,"Surface":725.0,"Duration":14400,"CoolingRate":-5.9976}},{"geometry":{"type":"Polygon","coordinates":[[[35.282,-0.141],[35.468,-0.141],[35.505,-0.085],[35.505,0.028],[35.468,0.085],[35.393,0.169],[35.319,0.169],[35.245,0.113],[35.134,-0.028],[35.134,-0.113],[35.282,-0.141]]]},"type":"Feature","properties":{"NumIdBirth":339,"CType":7,"LonTrajCellCG":[35.3552,35.4,35.43,35.41],"CRainRate":0.0,"ConvTypeMethod":1,"ConvType":2,"SeverityIntensity":1,"ConvTypeQuality":3,"MvtDirection":222,"CTPressure":21700.0,"NumIdCell":239,"CTReff":"","LonG":35.3552,"LatTrajCellCG":[-0.0181,-0.08,-0.08,-0.06],"ExpansionRate":666.6481,"CTPhase":"","LatG":-0.0181,"BTmin":260.76,"BTmoy":268.2,"CTCot":"","CTCwp":"","NbPosLightning":0,"SeverityType":"","PhaseLife":2,"MvtSpeed":1.838,"Surface":505.0,"Duration":2700,"CoolingRate":-19.152}},{"geometry":{"type":"Polygon","coordinates":[[[36.186,0.988],[36.262,0.988],[36.53,1.158],[36.569,1.214],[36.612,1.44],[36.613,1.497],[36.464,1.609],[36.35,1.609],[36.312,1.581],[36.119,1.411],[36.117,1.326],[36.149,1.016],[36.186,0.988]]]},"type":"Feature","properties":{"NumIdBirth":327,"CType":7,"LonTrajCellCG":[36.3612,36.39,36.46,36.49,36.57,36.63,36.69,36.69,36.87,36.93,36.95,37.01],"CRainRate":0.0,"ConvTypeMethod":1,"ConvType":2,"SeverityIntensity":0,"ConvTypeQuality":3,"MvtDirection":276,"CTPressure":26100.0,"NumIdCell":249,"CTReff":"","LonG":36.3612,"LatTrajCellCG":[1.3242,1.3,1.35,1.26,1.27,1.28,1.29,1.27,1.29,1.28,1.29,1.28],"ExpansionRate":91.0801,"CTPhase":"","LatG":1.3242,"BTmin":244.18,"BTmoy":267.09,"CTCot":"","CTCwp":"","NbPosLightning":0,"SeverityType":"","PhaseLife":2,"MvtSpeed":5.173,"Surface":1704.9999,"Duration":11700,"CoolingRate":-19.4832}},{"geometry":{"type":"Polygon","coordinates":[[[36.671,0.085],[36.824,0.085],[36.939,0.113],[36.939,0.198],[36.748,0.226],[36.558,0.226],[36.557,0.113],[36.671,0.085]]]},"type":"Feature","properties":{"NumIdBirth":308,"CType":6,"LonTrajCellCG":[36.7478,36.82],"CRainRate":0.0,"ConvTypeMethod":1,"ConvType":1,"SeverityIntensity":1,"ConvTypeQuality":3,"MvtDirection":272,"CTPressure":49300.0,"NumIdCell":274,"CTReff":"","LonG":36.7478,"LatTrajCellCG":[0.1511,0.15],"ExpansionRate":439.7041,"CTPhase":"","LatG":0.1511,"BTmin":266.9,"BTmoy":273.36,"CTCot":"","CTCwp":"","NbPosLightning":0,"SeverityType":"","PhaseLife":2,"MvtSpeed":7.531,"Surface":265.0,"Duration":900,"CoolingRate":-11.3832}},{"geometry":{"type":"Polygon","coordinates":[[[35.582,-0.648],[35.657,-0.648],[35.694,-0.62],[35.693,-0.536],[35.655,-0.423],[35.58,-0.282],[35.431,-0.282],[35.283,-0.394],[35.247,-0.479],[35.247,-0.563],[35.582,-0.648]]]},"type":"Feature","properties":{"NumIdBirth":387,"CType":7,"LonTrajCellCG":[35.4736,35.48,35.5,35.49,35.49],"CRainRate":0.0,"ConvTypeMethod":1,"ConvType":2,"SeverityIntensity":1,"ConvTypeQuality":3,"MvtDirection":250,"CTPressure":46500.0,"NumIdCell":280,"CTReff":"","LonG":35.4736,"LatTrajCellCG":[-0.4596,-0.46,-0.46,-0.5,-0.53],"ExpansionRate":851.9041,"CTPhase":"","LatG":-0.4596,"BTmin":263.8,"BTmoy":268.22,"CTCot":"","CTCwp":"","NbPosLightning":0,"SeverityType":"","PhaseLife":2,"MvtSpeed":3.77,"Surface":660.0,"Duration":3600,"CoolingRate":-4.9968}},{"geometry":{"type":"Polygon","coordinates":[[[34.624,-0.816],[34.697,-0.816],[34.734,-0.788],[34.732,-0.647],[34.694,-0.506],[34.657,-0.394],[34.511,-0.394],[34.512,-0.562],[34.624,-0.816]]]},"type":"Feature","properties":{"NumIdBirth":390,"CType":8,"LonTrajCellCG":[34.6227,34.6,34.35,34.34,34.39,34.66],"CRainRate":0.0,"ConvTypeMethod":1,"ConvType":1,"SeverityIntensity":0,"ConvTypeQuality":1,"MvtDirection":82,"CTPressure":12300.0,"NumIdCell":286,"CTReff":"","LonG":34.6227,"LatTrajCellCG":[-0.5805,-0.58,-0.67,-0.67,-0.65,-0.91],"ExpansionRate":-194.4,"CTPhase":"","LatG":-0.5805,"BTmin":222.86,"BTmoy":230.62,"CTCot":"","CTCwp":"","NbPosLightning":0,"SeverityType":"","PhaseLife":4,"MvtSpeed":7.069,"Surface":460.0,"Duration":4500,"CoolingRate":30.06}},{"geometry":{"type":"Polygon","coordinates":[[[34.366,-0.562],[34.512,-0.562],[34.511,-0.478],[34.365,-0.478],[34.366,-0.562]]]},"type":"Feature","properties":{"NumIdBirth":0,"CType":8,"LonTrajCellCG":[34.4385],"CRainRate":0.0,"ConvTypeMethod":1,"ConvType":2,"SeverityIntensity":0,"ConvTypeQuality":2,"MvtDirection":82,"CTPressure":19600.0,"NumIdCell":287,"CTReff":"","LonG":34.4385,"LatTrajCellCG":[-0.5202],"ExpansionRate":"","CTPhase":"","LatG":-0.5202,"BTmin":228.35,"BTmoy":231.94,"CTCot":"","CTCwp":"","NbPosLightning":0,"SeverityType":"","PhaseLife":1,"MvtSpeed":7.046,"Surface":75.0,"Duration":0,"CoolingRate":30.06}},{"geometry":{"type":"Polygon","coordinates":[[[30.286,-2.261],[30.354,-2.261],[30.49,-2.234],[30.523,-2.206],[30.555,-2.15],[30.617,-1.871],[30.616,-1.815],[30.546,-1.759],[30.443,-1.731],[30.306,-1.702],[30.238,-1.702],[30.138,-1.758],[30.105,-1.786],[30.04,-1.925],[30.043,-2.037],[30.079,-2.12],[30.114,-2.176],[30.149,-2.204],[30.217,-2.233],[30.286,-2.261]]]},"type":"Feature","properties":{"NumIdBirth":0,"CType":9,"LonTrajCellCG":[30.3335],"CRainRate":26.4,"ConvTypeMethod":1,"ConvType":2,"SeverityIntensity":0,"ConvTypeQuality":3,"MvtDirection":300,"CTPressure":7300.0,"NumIdCell":291,"CTReff":"","LonG":30.3335,"LatTrajCellCG":[-1.9806],"ExpansionRate":"","CTPhase":"","LatG":-1.9806,"BTmin":197.66,"BTmoy":208.31,"CTCot":"","CTCwp":"","NbPosLightning":0,"SeverityType":"","PhaseLife":1,"MvtSpeed":11.901,"Surface":2310.0001,"Duration":0,"CoolingRate":0.036}},{"geometry":{"type":"Polygon","coordinates":[[[30.757,-2.011],[30.826,-2.011],[30.859,-1.984],[30.857,-1.9],[30.786,-1.788],[30.717,-1.787],[30.683,-1.787],[30.616,-1.843],[30.617,-1.899],[30.653,-1.955],[30.688,-1.983],[30.757,-2.011]]]},"type":"Feature","properties":{"NumIdBirth":330,"CType":8,"LonTrajCellCG":[30.7428,30.59,30.79,30.77,30.77,30.79,30.82],"CRainRate":0.0,"ConvTypeMethod":1,"ConvType":2,"SeverityIntensity":0,"ConvTypeQuality":3,"MvtDirection":303,"CTPressure":15900.0,"NumIdCell":292,"CTReff":"","LonG":30.7428,"LatTrajCellCG":[-1.8982,-2.0,-2.02,-2.02,-2.05,-2.09,-2.17],"ExpansionRate":-374.112,"CTPhase":"","LatG":-1.8982,"BTmin":211.88,"BTmoy":215.71,"CTCot":"","CTCwp":"","NbPosLightning":0,"SeverityType":"","PhaseLife":3,"MvtSpeed":9.687,"Surface":285.0,"Duration":5400,"CoolingRate":0.036}},{"geometry":{"type":"Polygon","coordinates":[[[29.507,-3.235],[29.608,-3.235],[29.641,-3.208],[29.635,-3.04],[29.558,-2.76],[29.384,-2.592],[29.317,-2.564],[29.25,-2.564],[29.184,-2.591],[29.152,-2.647],[29.12,-2.703],[29.133,-3.093],[29.203,-3.177],[29.238,-3.205],[29.507,-3.235]]]},"type":"Feature","properties":{"NumIdBirth":399,"CType":9,"LonTrajCellCG":[29.3555,29.39,29.43,29.48,29.54,29.61,29.69,29.77,29.81],"CRainRate":28.5,"ConvTypeMethod":1,"ConvType":2,"SeverityIntensity":2,"ConvTypeQuality":3,"MvtDirection":329,"CTPressure":7000.0,"NumIdCell":295,"CTReff":"","LonG":29.3555,"LatTrajCellCG":[-2.9198,-3.0,-3.04,-3.04,-3.1,-3.11,-3.1,-3.12,-3.15],"ExpansionRate":478.6561,"CTPhase":"","LatG":-2.9198,"BTmin":194.86,"BTmoy":215.83,"CTCot":"","CTCwp":"","NbPosLightning":0,"SeverityType":"","PhaseLife":3,"MvtSpeed":9.269,"Surface":2564.9999,"Duration":7200,"CoolingRate":-35.5824}},{"geometry":{"type":"Polygon","coordinates":[[[29.949,-3.321],[29.98,-3.237],[29.975,-3.126],[29.908,-3.125],[29.874,-3.125],[29.841,-3.153],[29.848,-3.32],[29.949,-3.321]]]},"type":"Feature","properties":{"NumIdBirth":367,"CType":8,"LonTrajCellCG":[29.9064,29.96,29.99],"CRainRate":3.5,"ConvTypeMethod":1,"ConvType":2,"SeverityIntensity":2,"ConvTypeQuality":1,"MvtDirection":321,"CTPressure":18000.0,"NumIdCell":297,"CTReff":"","LonG":29.9064,"LatTrajCellCG":[-3.219,-3.28,-3.31],"ExpansionRate":1331.4962,"CTPhase":"","LatG":-3.219,"BTmin":228.64,"BTmoy":243.32,"CTCot":"","CTCwp":"","NbPosLightning":0,"SeverityType":"","PhaseLife":3,"MvtSpeed":8.24,"Surface":165.0,"Duration":1800,"CoolingRate":-26.4384}},{"geometry":{"type":"Polygon","coordinates":[[[29.212,-3.401],[29.346,-3.401],[29.378,-3.374],[29.374,-3.262],[29.339,-3.234],[29.239,-3.233],[29.205,-3.233],[29.212,-3.401]]]},"type":"Feature","properties":{"NumIdBirth":335,"CType":8,"LonTrajCellCG":[29.2868,29.34],"CRainRate":2.4,"ConvTypeMethod":1,"ConvType":1,"SeverityIntensity":0,"ConvTypeQuality":1,"MvtDirection":308,"CTPressure":15900.0,"NumIdCell":298,"CTReff":"","LonG":29.2868,"LatTrajCellCG":[-3.3173,-3.36],"ExpansionRate":284.256,"CTPhase":"","LatG":-3.3173,"BTmin":229.79,"BTmoy":245.07,"CTCot":"","CTCwp":"","NbPosLightning":0,"SeverityType":"","PhaseLife":2,"MvtSpeed":7.73,"Surface":210.0,"Duration":900,"CoolingRate":-16.6536}},{"geometry":{"type":"Polygon","coordinates":[[[29.185,-1.142],[29.352,-1.142],[29.384,-0.947],[29.383,-0.892],[29.249,-0.891],[29.216,-0.891],[29.149,-0.919],[29.152,-1.114],[29.185,-1.142]]]},"type":"Feature","properties":{"NumIdBirth":379,"CType":6,"LonTrajCellCG":[29.256,29.31,29.38,29.45,29.51,29.58,29.65],"CRainRate":0.0,"ConvTypeMethod":1,"ConvType":2,"SeverityIntensity":2,"ConvTypeQuality":3,"MvtDirection":280,"CTPressure":20400.0,"NumIdCell":311,"CTReff":"","LonG":29.256,"LatTrajCellCG":[-1.0193,-1.04,-1.07,-1.05,-1.06,-1.05,-1.04],"ExpansionRate":386.424,"CTPhase":"","LatG":-1.0193,"BTmin":228.93,"BTmoy":260.69,"CTCot":"","CTCwp":"","NbPosLightning":0,"SeverityType":"","PhaseLife":2,"MvtSpeed":7.735,"Surface":425.0,"Duration":5400,"CoolingRate":-70.884}},{"geometry":{"type":"Polygon","coordinates":[[[28.356,-1.196],[28.488,-1.196],[28.521,-1.168],[28.52,-1.113],[28.486,-1.029],[28.255,-1.029],[28.256,-1.112],[28.29,-1.168],[28.356,-1.196]]]},"type":"Feature","properties":{"NumIdBirth":404,"CType":12,"LonTrajCellCG":[28.3883,28.44,28.47,28.48,28.52,28.55,28.59],"CRainRate":0.0,"ConvTypeMethod":1,"ConvType":2,"SeverityIntensity":0,"ConvTypeQuality":1,"MvtDirection":339,"CTPressure":16500.0,"NumIdCell":312,"CTReff":"","LonG":28.3883,"LatTrajCellCG":[-1.1061,-1.17,-1.23,-1.39,-1.41,-1.46,-1.46],"ExpansionRate":-111.2399,"CTPhase":"","LatG":-1.1061,"BTmin":230.07,"BTmoy":256.61,"CTCot":"","CTCwp":"","NbPosLightning":0,"SeverityType":"","PhaseLife":4,"MvtSpeed":8.424,"Surface":295.0,"Duration":5400,"CoolingRate":10.476}},{"geometry":{"type":"Polygon","coordinates":[[[28.954,-1.281],[29.054,-1.281],[29.052,-1.17],[28.986,-1.169],[28.952,-1.169],[28.919,-1.197],[28.92,-1.253],[28.954,-1.281]]]},"type":"Feature","properties":{"NumIdBirth":395,"CType":8,"LonTrajCellCG":[28.9959,29.06,29.22,29.28,29.37,29.44,29.48],"CRainRate":0.0,"ConvTypeMethod":1,"ConvType":2,"SeverityIntensity":0,"ConvTypeQuality":3,"MvtDirection":306,"CTPressure":20700.0,"NumIdCell":316,"CTReff":"","LonG":28.9959,"LatTrajCellCG":[-1.2251,-1.36,-1.5,-1.53,-1.53,-1.55,-1.51],"ExpansionRate":-116.0639,"CTPhase":"","LatG":-1.2251,"BTmin":237.51,"BTmoy":250.13,"CTCot":"","CTCwp":"","NbPosLightning":0,"SeverityType":"","PhaseLife":4,"MvtSpeed":10.48,"Surface":80.0,"Duration":5400,"CoolingRate":7.0344}},{"geometry":{"type":"Polygon","coordinates":[[[28.393,-1.419],[28.492,-1.419],[28.557,-1.363],[28.556,-1.308],[28.522,-1.252],[28.489,-1.224],[28.357,-1.224],[28.324,-1.252],[28.326,-1.391],[28.393,-1.419]]]},"type":"Feature","properties":{"NumIdBirth":391,"CType":8,"LonTrajCellCG":[28.4293,28.44,28.48],"CRainRate":30.2,"ConvTypeMethod":1,"ConvType":2,"SeverityIntensity":2,"ConvTypeQuality":1,"MvtDirection":345,"CTPressure":7000.0,"NumIdCell":317,"CTReff":"","LonG":28.4293,"LatTrajCellCG":[-1.3192,-1.38,-1.38],"ExpansionRate":342.2881,"CTPhase":"","LatG":-1.3192,"BTmin":196.0,"BTmoy":222.71,"CTCot":"","CTCwp":"","NbPosLightning":0,"SeverityType":"","PhaseLife":2,"MvtSpeed":5.424,"Surface":295.0,"Duration":1800,"CoolingRate":-61.8048}},{"geometry":{"type":"Polygon","coordinates":[[[28.56,-1.558],[28.626,-1.558],[28.691,-1.475],[28.69,-1.419],[28.623,-1.363],[28.524,-1.363],[28.527,-1.53],[28.56,-1.558]]]},"type":"Feature","properties":{"NumIdBirth":400,"CType":8,"LonTrajCellCG":[28.5916,28.64,28.68,28.75,28.76],"CRainRate":2.7,"ConvTypeMethod":1,"ConvType":2,"SeverityIntensity":1,"ConvTypeQuality":1,"MvtDirection":324,"CTPressure":18300.0,"NumIdCell":318,"CTReff":"","LonG":28.5916,"LatTrajCellCG":[-1.4525,-1.5,-1.51,-1.55,-1.54],"ExpansionRate":198.3601,"CTPhase":"","LatG":-1.4525,"BTmin":224.44,"BTmoy":243.77,"CTCot":"","CTCwp":"","NbPosLightning":0,"SeverityType":"","PhaseLife":3,"MvtSpeed":5.606,"Surface":170.0,"Duration":3600,"CoolingRate":-26.8344}},{"geometry":{"type":"Polygon","coordinates":[[[34.377,-1.35],[34.522,-1.294],[34.52,-1.209],[34.338,-1.209],[34.266,-1.265],[34.268,-1.349],[34.377,-1.35]]]},"type":"Feature","properties":{"NumIdBirth":403,"CType":7,"LonTrajCellCG":[34.3884,34.43,34.51,34.55,34.64],"CRainRate":0.0,"ConvTypeMethod":1,"ConvType":1,"SeverityIntensity":1,"ConvTypeQuality":1,"MvtDirection":301,"CTPressure":43100.0,"NumIdCell":320,"CTReff":"","LonG":34.3884,"LatTrajCellCG":[-1.2713,-1.29,-1.32,-1.35,-1.36],"ExpansionRate":309.9601,"CTPhase":"","LatG":-1.2713,"BTmin":261.13,"BTmoy":270.5,"CTCot":"","CTCwp":"","NbPosLightning":0,"SeverityType":"","PhaseLife":2,"MvtSpeed":7.144,"Surface":175.0,"Duration":3600,"CoolingRate":-27.6984}},{"geometry":{"type":"Polygon","coordinates":[[[40.749,-1.681],[40.916,-1.681],[41.082,-1.654],[41.079,-1.539],[40.823,-1.368],[40.74,-1.367],[40.698,-1.367],[40.576,-1.452],[40.58,-1.595],[40.623,-1.623],[40.749,-1.681]]]},"type":"Feature","properties":{"NumIdBirth":406,"CType":7,"LonTrajCellCG":[40.8076,41.04,40.99,41.05,41.09],"CRainRate":0.0,"ConvTypeMethod":1,"ConvType":2,"SeverityIntensity":2,"ConvTypeQuality":2,"MvtDirection":249,"CTPressure":27800.0,"NumIdCell":323,"CTReff":"","LonG":40.8076,"LatTrajCellCG":[-1.5518,-1.58,-1.59,-1.57,-1.56],"ExpansionRate":1542.24,"CTPhase":"","LatG":-1.5518,"BTmin":244.86,"BTmoy":266.8,"CTCot":"","CTCwp":"","NbPosLightning":0,"SeverityType":"","PhaseLife":2,"MvtSpeed":5.372,"Surface":850.0,"Duration":3600,"CoolingRate":-46.368}},{"geometry":{"type":"Polygon","coordinates":[[[41.122,-1.597],[41.248,-1.597],[41.243,-1.426],[41.201,-1.397],[41.116,-1.368],[41.032,-1.368],[40.991,-1.397],[40.993,-1.482],[41.079,-1.568],[41.122,-1.597]]]},"type":"Feature","properties":{"NumIdBirth":405,"CType":7,"LonTrajCellCG":[41.1341,41.15,41.16,41.15,41.16],"CRainRate":0.0,"ConvTypeMethod":1,"ConvType":2,"SeverityIntensity":2,"ConvTypeQuality":1,"MvtDirection":245,"CTPressure":25600.0,"NumIdCell":324,"CTReff":"","LonG":41.1341,"LatTrajCellCG":[-1.4813,-1.47,-1.46,-1.45,-1.44],"ExpansionRate":476.136,"CTPhase":"","LatG":-1.4813,"BTmin":243.27,"BTmoy":261.47,"CTCot":"","CTCwp":"","NbPosLightning":0,"SeverityType":"","PhaseLife":2,"MvtSpeed":2.419,"Surface":325.0,"Duration":3600,"CoolingRate":-23.9328}},{"geometry":{"type":"Polygon","coordinates":[[[34.756,-1.914],[34.754,-1.829],[34.679,-1.773],[34.606,-1.772],[34.569,-1.772],[34.572,-1.885],[34.683,-1.913],[34.756,-1.914]]]},"type":"Feature","properties":{"NumIdBirth":375,"CType":6,"LonTrajCellCG":[34.6522,34.71],"CRainRate":0.0,"ConvTypeMethod":1,"ConvType":1,"SeverityIntensity":0,"ConvTypeQuality":3,"MvtDirection":285,"CTPressure":50500.0,"NumIdCell":328,"CTReff":"","LonG":34.6522,"LatTrajCellCG":[-1.8412,-1.84],"ExpansionRate":184.1761,"CTPhase":"","LatG":-1.8412,"BTmin":268.41,"BTmoy":274.58,"CTCot":"","CTCwp":"","NbPosLightning":0,"SeverityType":"","PhaseLife":2,"MvtSpeed":8.277,"Surface":115.0,"Duration":900,"CoolingRate":-12.9456}},{"geometry":{"type":"Polygon","coordinates":[[[28.294,-2.81],[28.359,-2.783],[28.779,-2.422],[28.777,-2.367],[28.607,-2.199],[28.473,-2.115],[28.406,-2.087],[27.908,-1.89],[27.81,-1.89],[27.713,-1.973],[27.587,-2.139],[27.556,-2.223],[27.558,-2.306],[27.592,-2.362],[27.831,-2.725],[27.865,-2.753],[27.965,-2.809],[28.294,-2.81]]]},"type":"Feature","properties":{"NumIdBirth":414,"CType":9,"LonTrajCellCG":[28.0836,28.11,28.47,28.5,28.52],"CRainRate":42.3,"ConvTypeMethod":1,"ConvType":2,"SeverityIntensity":0,"ConvTypeQuality":3,"MvtDirection":322,"CTPressure":7100.0,"NumIdCell":329,"CTReff":"","LonG":28.0836,"LatTrajCellCG":[-2.358,-2.38,-2.42,-2.43,-2.48],"ExpansionRate":73.8721,"CTPhase":"","LatG":-2.358,"BTmin":190.57,"BTmoy":219.21,"CTCot":"","CTCwp":"","NbPosLightning":0,"SeverityType":"","PhaseLife":3,"MvtSpeed":3.124,"Surface":7085.0002,"Duration":3600,"CoolingRate":-12.3336}},{"geometry":{"type":"Polygon","coordinates":[[[28.105,-3.06],[28.168,-3.005],[28.165,-2.893],[28.13,-2.837],[27.999,-2.837],[27.937,-2.976],[27.94,-3.059],[28.105,-3.06]]]},"type":"Feature","properties":{"NumIdBirth":397,"CType":8,"LonTrajCellCG":[28.06,28.1,28.18,28.25,28.35,28.39,28.45,28.5,28.53,28.58],"CRainRate":8.5,"ConvTypeMethod":1,"ConvType":1,"SeverityIntensity":2,"ConvTypeQuality":1,"MvtDirection":283,"CTPressure":15500.0,"NumIdCell":330,"CTReff":"","LonG":28.06,"LatTrajCellCG":[-2.9563,-2.98,-3.05,-3.1,-3.15,-3.17,-3.17,-3.14,-3.14,-3.14],"ExpansionRate":467.2081,"CTPhase":"","LatG":-2.9563,"BTmin":217.49,"BTmoy":242.03,"CTCot":"","CTCwp":"","NbPosLightning":0,"SeverityType":"","PhaseLife":2,"MvtSpeed":7.813,"Surface":315.0,"Duration":8100,"CoolingRate":-45.8568}},{"geometry":{"type":"Polygon","coordinates":[[[28.079,-3.255],[28.073,-3.088],[28.039,-3.06],[27.973,-3.059],[27.94,-3.059],[27.91,-3.143],[27.914,-3.254],[28.079,-3.255]]]},"type":"Feature","properties":{"NumIdBirth":379,"CType":8,"LonTrajCellCG":[27.9981,28.07],"CRainRate":0.0,"ConvTypeMethod":1,"ConvType":1,"SeverityIntensity":4,"ConvTypeQuality":1,"MvtDirection":292,"CTPressure":16500.0,"NumIdCell":331,"CTReff":"","LonG":27.9981,"LatTrajCellCG":[-3.1667,-3.21],"ExpansionRate":1608.9841,"CTPhase":"","LatG":-3.1667,"BTmin":225.37,"BTmoy":247.61,"CTCot":"","CTCwp":"","NbPosLightning":0,"SeverityType":"","PhaseLife":3,"MvtSpeed":8.481,"Surface":225.0,"Duration":900,"CoolingRate":-115.128}},{"geometry":{"type":"Polygon","coordinates":[[[28.205,-3.953],[28.399,-3.842],[28.46,-3.731],[28.454,-3.592],[28.385,-3.507],[28.184,-3.423],[28.118,-3.422],[28.085,-3.422],[28.02,-3.45],[27.925,-3.533],[27.896,-3.644],[27.9,-3.728],[28.038,-3.896],[28.072,-3.924],[28.14,-3.952],[28.205,-3.953]]]},"type":"Feature","properties":{"NumIdBirth":405,"CType":9,"LonTrajCellCG":[28.1784,28.15,28.14,28.11,28.07,28.04,28.0,27.98,28.0,28.02],"CRainRate":50.0,"ConvTypeMethod":1,"ConvType":2,"SeverityIntensity":1,"ConvTypeQuality":2,"MvtDirection":55,"CTPressure":7000.0,"NumIdCell":332,"CTReff":"","LonG":28.1784,"LatTrajCellCG":[-3.6761,-3.71,-3.75,-3.8,-3.81,-3.79,-3.81,-3.78,-3.78,-3.79],"ExpansionRate":-0.2159,"CTPhase":"","LatG":-3.6761,"BTmin":193.07,"BTmoy":198.81,"CTCot":"","CTCwp":"","NbPosLightning":0,"SeverityType":"","PhaseLife":3,"MvtSpeed":3.211,"Surface":2090.0,"Duration":8100,"CoolingRate":2.4984}},{"geometry":{"type":"Polygon","coordinates":[[[27.782,-4.034],[27.909,-3.951],[27.941,-3.923],[27.937,-3.839],[27.901,-3.756],[27.866,-3.7],[27.765,-3.643],[27.634,-3.642],[27.601,-3.642],[27.475,-3.753],[27.445,-3.808],[27.45,-3.92],[27.485,-3.976],[27.552,-4.032],[27.782,-4.034]]]},"type":"Feature","properties":{"NumIdBirth":436,"CType":9,"LonTrajCellCG":[27.686,27.71,27.76,27.8],"CRainRate":50.0,"ConvTypeMethod":1,"ConvType":2,"SeverityIntensity":2,"ConvTypeQuality":1,"MvtDirection":290,"CTPressure":7100.0,"NumIdCell":333,"CTReff":"","LonG":27.686,"LatTrajCellCG":[-3.8469,-3.87,-3.9,-3.84],"ExpansionRate":446.112,"CTPhase":"","LatG":-3.8469,"BTmin":191.84,"BTmoy":197.55,"CTCot":"","CTCwp":"","NbPosLightning":0,"SeverityType":"","PhaseLife":4,"MvtSpeed":0.906,"Surface":1455.0001,"Duration":2700,"CoolingRate":7.56}},{"geometry":{"type":"Polygon","coordinates":[[[28.933,-3.902],[28.965,-3.874],[28.962,-3.818],[28.796,-3.817],[28.762,-3.817],[28.766,-3.9],[28.933,-3.902]]]},"type":"Feature","properties":{"NumIdBirth":438,"CType":6,"LonTrajCellCG":[28.8567,28.89,28.92],"CRainRate":0.0,"ConvTypeMethod":1,"ConvType":2,"SeverityIntensity":0,"ConvTypeQuality":3,"MvtDirection":274,"CTPressure":42700.0,"NumIdCell":362,"CTReff":"","LonG":28.8567,"LatTrajCellCG":[-3.8576,-3.86,-3.85],"ExpansionRate":260.6401,"CTPhase":"","LatG":-3.8576,"BTmin":260.76,"BTmoy":269.99,"CTCot":"","CTCwp":"","NbPosLightning":0,"SeverityType":"","PhaseLife":2,"MvtSpeed":5.257,"Surface":105.0,"Duration":1800,"CoolingRate":-17.1504}},{"geometry":{"type":"Polygon","coordinates":[[[37.704,-4.25],[37.741,-4.222],[37.735,-4.137],[37.655,-4.108],[37.539,-4.107],[37.5,-4.107],[37.506,-4.192],[37.548,-4.249],[37.704,-4.25]]]},"type":"Feature","properties":{"NumIdBirth":411,"CType":7,"LonTrajCellCG":[37.6186,37.63],"CRainRate":0.0,"ConvTypeMethod":1,"ConvType":1,"SeverityIntensity":3,"ConvTypeQuality":1,"MvtDirection":262,"CTPressure":30300.0,"NumIdCell":365,"CTReff":"","LonG":37.6186,"LatTrajCellCG":[-4.1777,-4.15],"ExpansionRate":506.592,"CTPhase":"","LatG":-4.1777,"BTmin":251.09,"BTmoy":267.8,"CTCot":"","CTCwp":"","NbPosLightning":0,"SeverityType":"","PhaseLife":2,"MvtSpeed":4.017,"Surface":205.0,"Duration":900,"CoolingRate":-56.2248}},{"geometry":{"type":"Polygon","coordinates":[[[27.534,-4.981],[27.729,-4.954],[27.826,-4.927],[27.857,-4.899],[27.853,-4.816],[27.611,-4.59],[27.513,-4.59],[27.449,-4.617],[27.387,-4.672],[27.335,-4.923],[27.338,-4.979],[27.534,-4.981]]]},"type":"Feature","properties":{"NumIdBirth":441,"CType":9,"LonTrajCellCG":[27.5688,27.57,27.61,27.64,27.66,27.74,27.77],"CRainRate":27.4,"ConvTypeMethod":1,"ConvType":2,"SeverityIntensity":0,"ConvTypeQuality":1,"MvtDirection":332,"CTPressure":7000.0,"NumIdCell":368,"CTReff":"","LonG":27.5688,"LatTrajCellCG":[-4.8116,-4.85,-4.87,-4.89,-4.91,-4.92,-4.91],"ExpansionRate":72.0721,"CTPhase":"","LatG":-4.8116,"BTmin":201.79,"BTmoy":223.37,"CTCot":"","CTCwp":"","NbPosLightning":0,"SeverityType":"","PhaseLife":4,"MvtSpeed":4.162,"Surface":1275.0001,"Duration":5400,"CoolingRate":6.1416}},{"geometry":{"type":"Polygon","coordinates":[[[28.102,-5.153],[28.133,-5.125],[28.13,-5.069],[28.095,-5.041],[27.898,-5.039],[27.834,-5.067],[27.837,-5.123],[27.871,-5.151],[28.102,-5.153]]]},"type":"Feature","properties":{"NumIdBirth":457,"CType":9,"LonTrajCellCG":[27.9877,27.98,27.98,27.99,28.03,28.11],"CRainRate":21.6,"ConvTypeMethod":1,"ConvType":2,"SeverityIntensity":1,"ConvTypeQuality":1,"MvtDirection":1,"CTPressure":10200.0,"NumIdCell":369,"CTReff":"","LonG":27.9877,"LatTrajCellCG":[-5.0975,-5.14,-5.2,-5.19,-5.19,-5.16],"ExpansionRate":404.3521,"CTPhase":"","LatG":-5.0975,"BTmin":207.33,"BTmoy":210.33,"CTCot":"","CTCwp":"","NbPosLightning":0,"SeverityType":"","PhaseLife":3,"MvtSpeed":3.808,"Surface":215.0,"Duration":4500,"CoolingRate":1.908}},{"geometry":{"type":"Polygon","coordinates":[[[27.531,-5.512],[27.597,-5.512],[27.661,-5.485],[27.786,-5.374],[27.874,-5.207],[27.87,-5.123],[27.768,-5.066],[27.506,-5.064],[27.473,-5.064],[27.411,-5.119],[27.381,-5.175],[27.389,-5.315],[27.462,-5.455],[27.531,-5.512]]]},"type":"Feature","properties":{"NumIdBirth":463,"CType":9,"LonTrajCellCG":[27.6058,27.61,27.59,27.61,27.66,27.69],"CRainRate":47.8,"ConvTypeMethod":1,"ConvType":2,"SeverityIntensity":0,"ConvTypeQuality":1,"MvtDirection":350,"CTPressure":10000.0,"NumIdCell":370,"CTReff":"","LonG":27.6058,"LatTrajCellCG":[-5.2535,-5.27,-5.33,-5.35,-5.36,-5.39],"ExpansionRate":360.2881,"CTPhase":"","LatG":-5.2535,"BTmin":196.0,"BTmoy":203.59,"CTCot":"","CTCwp":"","NbPosLightning":0,"SeverityType":"","PhaseLife":4,"MvtSpeed":3.653,"Surface":1524.9999,"Duration":4500,"CoolingRate":4.6008}},{"geometry":{"type":"Polygon","coordinates":[[[28.204,-5.742],[28.329,-5.631],[28.359,-5.575],[28.388,-5.519],[28.376,-5.323],[28.342,-5.295],[28.242,-5.294],[28.209,-5.294],[28.113,-5.349],[28.051,-5.404],[27.99,-5.488],[27.96,-5.543],[27.964,-5.599],[28.0,-5.656],[28.105,-5.741],[28.204,-5.742]]]},"type":"Feature","properties":{"NumIdBirth":445,"CType":9,"LonTrajCellCG":[28.1934,28.09,28.21,28.15,28.25,28.18,28.19,28.21],"CRainRate":44.3,"ConvTypeMethod":1,"ConvType":2,"SeverityIntensity":0,"ConvTypeQuality":1,"MvtDirection":307,"CTPressure":10000.0,"NumIdCell":371,"CTReff":"","LonG":28.1934,"LatTrajCellCG":[-5.5121,-5.59,-5.57,-5.64,-5.5,-5.69,-5.75,-5.78],"ExpansionRate":-147.0239,"CTPhase":"","LatG":-5.5121,"BTmin":197.11,"BTmoy":200.29,"CTCot":"","CTCwp":"","NbPosLightning":0,"SeverityType":"","PhaseLife":3,"MvtSpeed":1.978,"Surface":1244.9999,"Duration":6300,"CoolingRate":-11.52}},{"geometry":{"type":"Polygon","coordinates":[[[28.02,-5.964],[28.051,-5.936],[28.079,-5.852],[28.072,-5.74],[27.967,-5.655],[27.901,-5.655],[27.837,-5.682],[27.744,-5.765],[27.716,-5.849],[27.722,-5.933],[27.789,-5.962],[28.02,-5.964]]]},"type":"Feature","properties":{"NumIdBirth":0,"CType":9,"LonTrajCellCG":[27.9054],"CRainRate":45.5,"ConvTypeMethod":1,"ConvType":2,"SeverityIntensity":0,"ConvTypeQuality":1,"MvtDirection":272,"CTPressure":10000.0,"NumIdCell":372,"CTReff":"","LonG":27.9054,"LatTrajCellCG":[-5.8215],"ExpansionRate":"","CTPhase":"","LatG":-5.8215,"BTmin":194.27,"BTmoy":199.82,"CTCot":"","CTCwp":"","NbPosLightning":0,"SeverityType":"","PhaseLife":1,"MvtSpeed":2.789,"Surface":740.0,"Duration":0,"CoolingRate":-11.52}},{"geometry":{"type":"Polygon","coordinates":[[[28.469,-5.212],[28.501,-5.184],[28.497,-5.128],[28.459,-5.044],[28.425,-5.016],[28.325,-5.015],[28.292,-5.015],[28.261,-5.042],[28.265,-5.126],[28.302,-5.183],[28.337,-5.211],[28.469,-5.212]]]},"type":"Feature","properties":{"NumIdBirth":419,"CType":8,"LonTrajCellCG":[28.3772,28.42,28.47,28.52,28.54,28.55,28.59,28.6,28.66],"CRainRate":0.0,"ConvTypeMethod":1,"ConvType":1,"SeverityIntensity":0,"ConvTypeQuality":1,"MvtDirection":333,"CTPressure":15200.0,"NumIdCell":373,"CTReff":"","LonG":28.3772,"LatTrajCellCG":[-5.1128,-5.16,-5.22,-5.28,-5.35,-5.39,-5.45,-5.47,-5.48],"ExpansionRate":-43.056,"CTPhase":"","LatG":-5.1128,"BTmin":217.49,"BTmoy":227.88,"CTCot":"","CTCwp":"","NbPosLightning":0,"SeverityType":"","PhaseLife":4,"MvtSpeed":8.475,"Surface":285.0,"Duration":7200,"CoolingRate":4.14}},{"geometry":{"type":"Polygon","coordinates":[[[28.257,-6.949],[28.35,-6.865],[28.379,-6.809],[28.424,-6.529],[28.416,-6.417],[28.379,-6.36],[28.341,-6.304],[28.096,-6.105],[27.925,-6.019],[27.859,-6.018],[27.826,-6.018],[27.563,-6.492],[27.569,-6.576],[27.68,-6.746],[27.82,-6.859],[27.888,-6.888],[27.957,-6.917],[28.191,-6.948],[28.257,-6.949]]]},"type":"Feature","properties":{"NumIdBirth":485,"CType":9,"LonTrajCellCG":[28.0165,28.21,28.23,28.26,28.28,28.32],"CRainRate":46.7,"ConvTypeMethod":1,"ConvType":2,"SeverityIntensity":0,"ConvTypeQuality":1,"MvtDirection":284,"CTPressure":10000.0,"NumIdCell":374,"CTReff":"","LonG":28.0165,"LatTrajCellCG":[-6.5096,-6.77,-6.79,-6.82,-6.82,-6.85],"ExpansionRate":"","CTPhase":"","LatG":-6.5096,"BTmin":191.21,"BTmoy":223.65,"CTCot":"","CTCwp":"","NbPosLightning":0,"SeverityType":"","PhaseLife":2,"MvtSpeed":6.276,"Surface":5360.0,"Duration":4500,"CoolingRate":-14.5656}},{"geometry":{"type":"Polygon","coordinates":[[[27.586,-6.352],[27.679,-6.269],[27.71,-6.241],[27.7,-6.101],[27.598,-6.044],[27.532,-6.043],[27.347,-6.209],[27.351,-6.265],[27.42,-6.322],[27.487,-6.351],[27.586,-6.352]]]},"type":"Feature","properties":{"NumIdBirth":475,"CType":8,"LonTrajCellCG":[27.5487,27.6,27.66,27.7,27.75,27.8],"CRainRate":7.4,"ConvTypeMethod":1,"ConvType":2,"SeverityIntensity":0,"ConvTypeQuality":1,"MvtDirection":293,"CTPressure":14000.0,"NumIdCell":375,"CTReff":"","LonG":27.5487,"LatTrajCellCG":[-6.2061,-6.23,-6.24,-6.25,-6.23,-6.23],"ExpansionRate":-0.9359,"CTPhase":"","LatG":-6.2061,"BTmin":219.22,"BTmoy":254.0,"CTCot":"","CTCwp":"","NbPosLightning":0,"SeverityType":"","PhaseLife":4,"MvtSpeed":4.833,"Surface":600.0,"Duration":4500,"CoolingRate":11.1024}},{"geometry":{"type":"Polygon","coordinates":[[[38.806,-6.744],[38.84,-6.687],[38.87,-6.602],[38.86,-6.516],[38.727,-6.4],[38.647,-6.398],[38.613,-6.455],[38.646,-6.742],[38.806,-6.744]]]},"type":"Feature","properties":{"NumIdBirth":456,"CType":8,"LonTrajCellCG":[38.7259,38.74,38.77],"CRainRate":4.2,"ConvTypeMethod":1,"ConvType":2,"SeverityIntensity":1,"ConvTypeQuality":1,"MvtDirection":222,"CTPressure":11200.0,"NumIdCell":386,"CTReff":"","LonG":38.7259,"LatTrajCellCG":[-6.5855,-6.59,-6.56],"ExpansionRate":184.2481,"CTPhase":"","LatG":-6.5855,"BTmin":224.75,"BTmoy":247.18,"CTCot":"","CTCwp":"","NbPosLightning":0,"SeverityType":"","PhaseLife":2,"MvtSpeed":3.337,"Surface":510.0,"Duration":1800,"CoolingRate":-23.5944}},{"geometry":{"type":"Polygon","coordinates":[[[39.178,-7.467],[39.226,-7.209],[39.208,-7.065],[39.2,-7.008],[39.153,-6.95],[39.062,-6.862],[38.85,-6.773],[38.689,-6.771],[38.653,-6.799],[38.644,-7.057],[38.651,-7.114],[38.698,-7.172],[39.012,-7.435],[39.097,-7.465],[39.178,-7.467]]]},"type":"Feature","properties":{"NumIdBirth":459,"CType":8,"LonTrajCellCG":[38.9362,39.02,39.0,39.03],"CRainRate":0.0,"ConvTypeMethod":1,"ConvType":2,"SeverityIntensity":0,"ConvTypeQuality":1,"MvtDirection":218,"CTPressure":20500.0,"NumIdCell":387,"CTReff":"","LonG":38.9362,"LatTrajCellCG":[-7.0704,-7.04,-6.91,-6.87],"ExpansionRate":-313.3439,"CTPhase":"","LatG":-7.0704,"BTmin":232.29,"BTmoy":245.65,"CTCot":"","CTCwp":"","NbPosLightning":0,"SeverityType":"","PhaseLife":4,"MvtSpeed":10.482,"Surface":2260.0,"Duration":2700,"CoolingRate":26.5536}},{"geometry":{"type":"Polygon","coordinates":[[[34.531,-8.252],[34.513,-8.109],[34.46,-7.966],[34.41,-7.851],[34.299,-7.849],[34.266,-7.877],[34.289,-8.077],[34.373,-8.164],[34.457,-8.251],[34.531,-8.252]]]},"type":"Feature","properties":{"NumIdBirth":476,"CType":13,"LonTrajCellCG":[34.3883,34.52,34.6,34.66],"CRainRate":0.0,"ConvTypeMethod":1,"ConvType":1,"SeverityIntensity":0,"ConvTypeQuality":2,"MvtDirection":348,"CTPressure":16700.0,"NumIdCell":401,"CTReff":"","LonG":34.3883,"LatTrajCellCG":[-8.0297,-8.09,-8.19,-8.27],"ExpansionRate":-336.888,"CTPhase":"","LatG":-8.0297,"BTmin":226.88,"BTmoy":240.79,"CTCot":"","CTCwp":"","NbPosLightning":0,"SeverityType":"","PhaseLife":4,"MvtSpeed":13.964,"Surface":455.0,"Duration":2700,"CoolingRate":26.5104}},{"geometry":{"type":"Polygon","coordinates":[[[35.009,-8.517],[35.042,-8.489],[35.073,-8.432],[35.03,-8.089],[34.945,-8.002],[34.796,-7.999],[34.54,-8.024],[34.582,-8.367],[34.744,-8.484],[34.86,-8.514],[35.009,-8.517]]]},"type":"Feature","properties":{"NumIdBirth":494,"CType":8,"LonTrajCellCG":[34.8264,34.89,34.96],"CRainRate":13.7,"ConvTypeMethod":1,"ConvType":2,"SeverityIntensity":0,"ConvTypeQuality":1,"MvtDirection":346,"CTPressure":11500.0,"NumIdCell":402,"CTReff":"","LonG":34.8264,"LatTrajCellCG":[-8.2488,-8.3,-8.34],"ExpansionRate":396.72,"CTPhase":"","LatG":-8.2488,"BTmin":209.45,"BTmoy":230.28,"CTCot":"","CTCwp":"","NbPosLightning":0,"SeverityType":"","PhaseLife":3,"MvtSpeed":8.504,"Surface":2084.9999,"Duration":1800,"CoolingRate":0.0072}},{"geometry":{"type":"Polygon","coordinates":[[[35.146,-8.119],[35.401,-8.066],[35.432,-8.009],[35.493,-7.896],[35.486,-7.839],[35.445,-7.81],[35.295,-7.807],[35.258,-7.807],[35.05,-7.946],[35.016,-7.974],[35.027,-8.06],[35.071,-8.118],[35.146,-8.119]]]},"type":"Feature","properties":{"NumIdBirth":474,"CType":7,"LonTrajCellCG":[35.2631,35.27,35.29,35.27],"CRainRate":0.0,"ConvTypeMethod":1,"ConvType":1,"SeverityIntensity":1,"ConvTypeQuality":3,"MvtDirection":259,"CTPressure":27700.0,"NumIdCell":403,"CTReff":"","LonG":35.2631,"LatTrajCellCG":[-7.961,-7.88,-7.89,-7.94],"ExpansionRate":218.6641,"CTPhase":"","LatG":-7.961,"BTmin":244.41,"BTmoy":260.4,"CTCot":"","CTCwp":"","NbPosLightning":0,"SeverityType":"","PhaseLife":2,"MvtSpeed":3.657,"Surface":685.0,"Duration":2700,"CoolingRate":-34.1712}},{"geometry":{"type":"Polygon","coordinates":[[[41.836,-7.394],[41.854,-7.221],[41.838,-7.106],[41.783,-7.018],[41.736,-6.989],[41.565,-6.986],[41.487,-7.042],[41.418,-7.157],[41.426,-7.214],[41.48,-7.302],[41.707,-7.392],[41.836,-7.394]]]},"type":"Feature","properties":{"NumIdBirth":457,"CType":7,"LonTrajCellCG":[41.6565,41.69],"CRainRate":15.7,"ConvTypeMethod":1,"ConvType":2,"SeverityIntensity":3,"ConvTypeQuality":2,"MvtDirection":319,"CTPressure":16600.0,"NumIdCell":412,"CTReff":"","LonG":41.6565,"LatTrajCellCG":[-7.1754,-7.19],"ExpansionRate":850.8961,"CTPhase":"","LatG":-7.1754,"BTmin":224.13,"BTmoy":260.47,"CTCot":"","CTCwp":"","NbPosLightning":0,"SeverityType":"","PhaseLife":3,"MvtSpeed":3.556,"Surface":1090.0,"Duration":900,"CoolingRate":-72.0792}},{"geometry":{"type":"Polygon","coordinates":[[[28.432,-7.457],[28.46,-7.401],[28.455,-7.345],[28.417,-7.288],[28.284,-7.286],[28.251,-7.286],[28.192,-7.37],[28.196,-7.426],[28.265,-7.455],[28.432,-7.457]]]},"type":"Feature","properties":{"NumIdBirth":0,"CType":9,"LonTrajCellCG":[28.3331],"CRainRate":33.2,"ConvTypeMethod":1,"ConvType":2,"SeverityIntensity":0,"ConvTypeQuality":1,"MvtDirection":73,"CTPressure":10300.0,"NumIdCell":415,"CTReff":"","LonG":28.3331,"LatTrajCellCG":[-7.3738],"ExpansionRate":"","CTPhase":"","LatG":-7.3738,"BTmin":197.66,"BTmoy":200.98,"CTCot":"","CTCwp":"","NbPosLightning":0,"SeverityType":"","PhaseLife":1,"MvtSpeed":4.56,"Surface":275.0,"Duration":0,"CoolingRate":-2.3616}},{"geometry":{"type":"Polygon","coordinates":[[[28.51,-7.599],[28.541,-7.571],[28.532,-7.458],[28.432,-7.457],[28.398,-7.457],[28.372,-7.541],[28.377,-7.597],[28.51,-7.599]]]},"type":"Feature","properties":{"NumIdBirth":473,"CType":9,"LonTrajCellCG":[28.4627,28.28,28.3,28.3,28.3,28.34,28.39],"CRainRate":19.7,"ConvTypeMethod":1,"ConvType":2,"SeverityIntensity":0,"ConvTypeQuality":1,"MvtDirection":73,"CTPressure":10100.0,"NumIdCell":416,"CTReff":"","LonG":28.4627,"LatTrajCellCG":[-7.528,-7.48,-7.49,-7.48,-7.49,-7.5,-7.48],"ExpansionRate":-269.2799,"CTPhase":"","LatG":-7.528,"BTmin":196.56,"BTmoy":200.36,"CTCot":"","CTCwp":"","NbPosLightning":0,"SeverityType":"","PhaseLife":2,"MvtSpeed":4.567,"Surface":140.0,"Duration":5400,"CoolingRate":-2.3616}},{"geometry":{"type":"Polygon","coordinates":[[[29.136,-8.2],[29.201,-8.173],[29.325,-8.061],[29.353,-8.005],[29.465,-7.752],[29.471,-7.442],[29.461,-7.329],[29.253,-7.27],[29.186,-7.269],[29.152,-7.269],[28.955,-7.323],[28.89,-7.35],[28.644,-7.601],[28.59,-7.741],[28.595,-7.797],[28.649,-8.024],[28.796,-8.167],[28.832,-8.196],[29.136,-8.2]]]},"type":"Feature","properties":{"NumIdBirth":464,"CType":9,"LonTrajCellCG":[29.0476,29.42],"CRainRate":34.3,"ConvTypeMethod":1,"ConvType":2,"SeverityIntensity":0,"ConvTypeQuality":1,"MvtDirection":303,"CTPressure":7500.0,"NumIdCell":418,"CTReff":"","LonG":29.0476,"LatTrajCellCG":[-7.7587,-7.71],"ExpansionRate":"","CTPhase":"","LatG":-7.7587,"BTmin":186.46,"BTmoy":224.3,"CTCot":"","CTCwp":"","NbPosLightning":0,"SeverityType":"","PhaseLife":2,"MvtSpeed":5.798,"Surface":6000.0,"Duration":900,"CoolingRate":-16.3584}},{"geometry":{"type":"Polygon","coordinates":[[[38.518,-7.944],[38.595,-7.917],[38.579,-7.802],[38.44,-7.656],[38.241,-7.653],[38.209,-7.71],[38.188,-7.853],[38.195,-7.91],[38.239,-7.94],[38.518,-7.944]]]},"type":"Feature","properties":{"NumIdBirth":485,"CType":8,"LonTrajCellCG":[38.373,38.37,38.38],"CRainRate":7.2,"ConvTypeMethod":1,"ConvType":2,"SeverityIntensity":0,"ConvTypeQuality":1,"MvtDirection":185,"CTPressure":13100.0,"NumIdCell":422,"CTReff":"","LonG":38.373,"LatTrajCellCG":[-7.8154,-7.8,-7.79],"ExpansionRate":206.7841,"CTPhase":"","LatG":-7.8154,"BTmin":216.77,"BTmoy":246.47,"CTCot":"","CTCwp":"","NbPosLightning":0,"SeverityType":"","PhaseLife":4,"MvtSpeed":2.136,"Surface":725.0,"Duration":1800,"CoolingRate":10.4904}},{"geometry":{"type":"Polygon","coordinates":[[[39.027,-8.414],[39.063,-8.385],[39.095,-8.328],[39.083,-8.242],[38.993,-8.183],[38.868,-8.152],[38.787,-8.15],[38.747,-8.15],[38.715,-8.207],[38.732,-8.322],[38.78,-8.38],[38.865,-8.411],[39.027,-8.414]]]},"type":"Feature","properties":{"NumIdBirth":471,"CType":8,"LonTrajCellCG":[38.8998,38.88],"CRainRate":9.1,"ConvTypeMethod":1,"ConvType":1,"SeverityIntensity":1,"ConvTypeQuality":1,"MvtDirection":186,"CTPressure":14100.0,"NumIdCell":428,"CTReff":"","LonG":38.8998,"LatTrajCellCG":[-8.2846,-8.24],"ExpansionRate":1610.2799,"CTPhase":"","LatG":-8.2846,"BTmin":222.86,"BTmoy":239.91,"CTCot":"","CTCwp":"","NbPosLightning":0,"SeverityType":"","PhaseLife":2,"MvtSpeed":7.617,"Surface":650.0,"Duration":900,"CoolingRate":-14.7456}},{"geometry":{"type":"Polygon","coordinates":[[[37.823,-8.364],[37.961,-8.222],[37.953,-8.165],[37.909,-8.135],[37.827,-8.105],[37.708,-8.103],[37.677,-8.16],[37.7,-8.333],[37.744,-8.362],[37.823,-8.364]]]},"type":"Feature","properties":{"NumIdBirth":499,"CType":7,"LonTrajCellCG":[37.7901,37.76,37.83],"CRainRate":0.0,"ConvTypeMethod":1,"ConvType":2,"SeverityIntensity":3,"ConvTypeQuality":3,"MvtDirection":303,"CTPressure":20100.0,"NumIdCell":431,"CTReff":"","LonG":37.7901,"LatTrajCellCG":[-8.2184,-8.24,-8.21],"ExpansionRate":1030.3921,"CTPhase":"","LatG":-8.2184,"BTmin":244.41,"BTmoy":257.65,"CTCot":"","CTCwp":"","NbPosLightning":0,"SeverityType":"","PhaseLife":2,"MvtSpeed":2.959,"Surface":350.0,"Duration":1800,"CoolingRate":-70.5744}},{"geometry":{"type":"Polygon","coordinates":[[[38.006,-8.539],[38.077,-8.483],[38.064,-8.397],[38.021,-8.367],[37.902,-8.365],[37.87,-8.422],[37.883,-8.509],[37.926,-8.538],[38.006,-8.539]]]},"type":"Feature","properties":{"NumIdBirth":516,"CType":8,"LonTrajCellCG":[37.9713,37.99,38.03,38.05,38.19,38.13],"CRainRate":9.5,"ConvTypeMethod":1,"ConvType":2,"SeverityIntensity":0,"ConvTypeQuality":2,"MvtDirection":329,"CTPressure":19900.0,"NumIdCell":432,"CTReff":"","LonG":37.9713,"LatTrajCellCG":[-8.4481,-8.47,-8.49,-8.52,-8.57,-8.58],"ExpansionRate":143.928,"CTPhase":"","LatG":-8.4481,"BTmin":222.54,"BTmoy":230.13,"CTCot":"","CTCwp":"","NbPosLightning":0,"SeverityType":"","PhaseLife":3,"MvtSpeed":4.213,"Surface":185.0,"Duration":4500,"CoolingRate":-6.4296}},{"geometry":{"type":"Polygon","coordinates":[[[37.74,-8.88],[37.991,-8.712],[38.027,-8.684],[38.006,-8.539],[37.748,-8.391],[37.512,-8.387],[37.477,-8.415],[37.445,-8.472],[37.495,-8.818],[37.582,-8.877],[37.74,-8.88]]]},"type":"Feature","properties":{"NumIdBirth":477,"CType":8,"LonTrajCellCG":[37.687,37.67,37.68,37.82,37.85,37.8,37.89,37.97],"CRainRate":28.3,"ConvTypeMethod":1,"ConvType":2,"SeverityIntensity":0,"ConvTypeQuality":1,"MvtDirection":354,"CTPressure":11000.0,"NumIdCell":433,"CTReff":"","LonG":37.687,"LatTrajCellCG":[-8.6297,-8.68,-8.68,-8.49,-8.53,-8.52,-8.58,-8.58],"ExpansionRate":236.664,"CTPhase":"","LatG":-8.6297,"BTmin":200.79,"BTmoy":219.25,"CTCot":"","CTCwp":"","NbPosLightning":0,"SeverityType":"","PhaseLife":3,"MvtSpeed":3.258,"Surface":1895.0001,"Duration":6300,"CoolingRate":-3.8376}},{"geometry":{"type":"Polygon","coordinates":[[[37.482,-9.509],[37.499,-9.365],[37.49,-9.307],[37.393,-9.19],[37.276,-9.188],[37.307,-9.39],[37.359,-9.478],[37.403,-9.507],[37.482,-9.509]]]},"type":"Feature","properties":{"NumIdBirth":441,"CType":8,"LonTrajCellCG":[37.3821,37.42,37.47,37.5,37.53,37.55,37.58,37.58,37.49,37.52],"CRainRate":13.4,"ConvTypeMethod":1,"ConvType":2,"SeverityIntensity":0,"ConvTypeQuality":3,"MvtDirection":349,"CTPressure":16700.0,"NumIdCell":435,"CTReff":"","LonG":37.3821,"LatTrajCellCG":[-9.3471,-9.4,-9.46,-9.51,-9.64,-9.64,-9.74,-9.74,-9.91,-9.78],"ExpansionRate":-295.848,"CTPhase":"","LatG":-9.3471,"BTmin":217.84,"BTmoy":223.17,"CTCot":"","CTCwp":"","NbPosLightning":0,"SeverityType":"","PhaseLife":4,"MvtSpeed":7.308,"Surface":280.0,"Duration":8100,"CoolingRate":9.9504}},{"geometry":{"type":"Polygon","coordinates":[[[37.206,-10.225],[37.393,-10.171],[37.428,-10.143],[37.492,-10.058],[37.468,-9.913],[37.377,-9.593],[37.324,-9.506],[37.242,-9.475],[36.968,-9.47],[36.922,-9.671],[36.996,-10.134],[37.128,-10.224],[37.206,-10.225]]]},"type":"Feature","properties":{"NumIdBirth":491,"CType":8,"LonTrajCellCG":[37.1876,37.24,37.28,37.33,37.34,37.39,37.44,37.47],"CRainRate":23.3,"ConvTypeMethod":1,"ConvType":2,"SeverityIntensity":0,"ConvTypeQuality":3,"MvtDirection":336,"CTPressure":11600.0,"NumIdCell":436,"CTReff":"","LonG":37.1876,"LatTrajCellCG":[-9.8355,-9.87,-9.93,-10.0,-10.03,-10.04,-9.97,-9.91],"ExpansionRate":-3.3119,"CTPhase":"","LatG":-9.8355,"BTmin":206.0,"BTmoy":221.88,"CTCot":"","CTCwp":"","NbPosLightning":0,"SeverityType":"","PhaseLife":4,"MvtSpeed":6.686,"Surface":3244.9999,"Duration":6300,"CoolingRate":5.4144}},{"geometry":{"type":"Polygon","coordinates":[[[36.483,-8.513],[36.517,-8.485],[36.502,-8.37],[36.452,-8.283],[36.375,-8.282],[36.345,-8.339],[36.352,-8.396],[36.406,-8.512],[36.483,-8.513]]]},"type":"Feature","properties":{"NumIdBirth":0,"CType":8,"LonTrajCellCG":[36.4367],"CRainRate":0.0,"ConvTypeMethod":1,"ConvType":2,"SeverityIntensity":0,"ConvTypeQuality":1,"MvtDirection":359,"CTPressure":20100.0,"NumIdCell":443,"CTReff":"","LonG":36.4367,"LatTrajCellCG":[-8.4028],"ExpansionRate":"","CTPhase":"","LatG":-8.4028,"BTmin":226.58,"BTmoy":234.52,"CTCot":"","CTCwp":"","NbPosLightning":0,"SeverityType":"","PhaseLife":1,"MvtSpeed":10.167,"Surface":150.0,"Duration":0,"CoolingRate":10.9152}},{"geometry":{"type":"Polygon","coordinates":[[[36.61,-8.601],[36.591,-8.457],[36.514,-8.456],[36.479,-8.484],[36.495,-8.599],[36.61,-8.601]]]},"type":"Feature","properties":{"NumIdBirth":409,"CType":8,"LonTrajCellCG":[36.5461,36.49,36.53,36.62,36.48,36.48,36.46,36.46,36.49,36.5,36.52,36.55],"CRainRate":0.0,"ConvTypeMethod":1,"ConvType":2,"SeverityIntensity":0,"ConvTypeQuality":1,"MvtDirection":360,"CTPressure":22500.0,"NumIdCell":444,"CTReff":"","LonG":36.5461,"LatTrajCellCG":[-8.5346,-8.57,-8.59,-8.67,-8.66,-8.75,-8.77,-8.81,-8.82,-8.83,-8.83,-8.84],"ExpansionRate":-326.5199,"CTPhase":"","LatG":-8.5346,"BTmin":232.01,"BTmoy":236.01,"CTCot":"","CTCwp":"","NbPosLightning":0,"SeverityType":"","PhaseLife":4,"MvtSpeed":10.177,"Surface":95.0,"Duration":9900,"CoolingRate":10.9152}},{"geometry":{"type":"Polygon","coordinates":[[[36.268,-9.169],[36.379,-9.143],[36.401,-9.028],[36.415,-8.856],[36.407,-8.799],[36.361,-8.74],[36.222,-8.566],[36.138,-8.507],[36.02,-8.476],[35.944,-8.475],[35.906,-8.475],[35.856,-8.674],[35.876,-8.818],[35.98,-9.021],[36.03,-9.108],[36.153,-9.167],[36.268,-9.169]]]},"type":"Feature","properties":{"NumIdBirth":0,"CType":8,"LonTrajCellCG":[36.1318],"CRainRate":6.8,"ConvTypeMethod":1,"ConvType":2,"SeverityIntensity":0,"ConvTypeQuality":2,"MvtDirection":348,"CTPressure":17700.0,"NumIdCell":445,"CTReff":"","LonG":36.1318,"LatTrajCellCG":[-8.8297],"ExpansionRate":"","CTPhase":"","LatG":-8.8297,"BTmin":217.49,"BTmoy":227.7,"CTCot":"","CTCwp":"","NbPosLightning":0,"SeverityType":"","PhaseLife":1,"MvtSpeed":8.778,"Surface":2590.0001,"Duration":0,"CoolingRate":4.14}},{"geometry":{"type":"Polygon","coordinates":[[[36.529,-9.117],[36.559,-9.06],[36.539,-8.916],[36.458,-8.886],[36.381,-8.884],[36.363,-9.027],[36.371,-9.085],[36.413,-9.115],[36.529,-9.117]]]},"type":"Feature","properties":{"NumIdBirth":411,"CType":8,"LonTrajCellCG":[36.4639,36.21,36.23,36.26,36.29,36.33,36.35,36.36,36.38,36.4,36.33],"CRainRate":6.5,"ConvTypeMethod":1,"ConvType":2,"SeverityIntensity":0,"ConvTypeQuality":2,"MvtDirection":351,"CTPressure":16100.0,"NumIdCell":446,"CTReff":"","LonG":36.4639,"LatTrajCellCG":[-9.0054,-8.92,-8.97,-9.02,-9.1,-9.17,-9.22,-9.25,-9.26,-9.26,-9.23],"ExpansionRate":-376.6319,"CTPhase":"","LatG":-9.0054,"BTmin":222.22,"BTmoy":233.01,"CTCot":"","CTCwp":"","NbPosLightning":0,"SeverityType":"","PhaseLife":4,"MvtSpeed":8.265,"Surface":245.0,"Duration":9000,"CoolingRate":4.14}},{"geometry":{"type":"Polygon","coordinates":[[[35.868,-9.564],[35.94,-9.537],[36.08,-9.453],[36.148,-9.397],[36.119,-9.195],[35.926,-8.905],[35.812,-8.903],[35.714,-9.016],[35.792,-9.563],[35.868,-9.564]]]},"type":"Feature","properties":{"NumIdBirth":516,"CType":8,"LonTrajCellCG":[35.913,35.95,36.01,36.05],"CRainRate":20.2,"ConvTypeMethod":1,"ConvType":2,"SeverityIntensity":0,"ConvTypeQuality":2,"MvtDirection":339,"CTPressure":15000.0,"NumIdCell":447,"CTReff":"","LonG":35.913,"LatTrajCellCG":[-9.2634,-9.27,-9.33,-9.44],"ExpansionRate":284.0401,"CTPhase":"","LatG":-9.2634,"BTmin":211.49,"BTmoy":227.34,"CTCot":"","CTCwp":"","NbPosLightning":0,"SeverityType":"","PhaseLife":3,"MvtSpeed":5.914,"Surface":1610.0,"Duration":2700,"CoolingRate":0.144}},{"geometry":{"type":"Polygon","coordinates":[[[35.704,-9.734],[35.733,-9.677],[35.72,-9.59],[35.606,-9.588],[35.568,-9.588],[35.539,-9.644],[35.552,-9.731],[35.704,-9.734]]]},"type":"Feature","properties":{"NumIdBirth":492,"CType":6,"LonTrajCellCG":[35.636,35.67],"CRainRate":0.0,"ConvTypeMethod":1,"ConvType":1,"SeverityIntensity":3,"ConvTypeQuality":1,"MvtDirection":326,"CTPressure":40100.0,"NumIdCell":448,"CTReff":"","LonG":35.636,"LatTrajCellCG":[-9.6607,-9.65],"ExpansionRate":913.32,"CTPhase":"","LatG":-9.6607,"BTmin":256.27,"BTmoy":268.3,"CTCot":"","CTCwp":"","NbPosLightning":0,"SeverityType":"","PhaseLife":2,"MvtSpeed":5.392,"Surface":160.0,"Duration":900,"CoolingRate":-59.1192}},{"geometry":{"type":"Polygon","coordinates":[[[42.206,-9.372],[42.228,-9.256],[42.235,-9.053],[42.224,-8.994],[41.897,-8.611],[41.849,-8.581],[41.763,-8.579],[41.729,-8.636],[41.744,-8.724],[41.853,-9.103],[41.912,-9.192],[41.966,-9.251],[42.119,-9.37],[42.206,-9.372]]]},"type":"Feature","properties":{"NumIdBirth":499,"CType":8,"LonTrajCellCG":[41.9944,42.03,42.01,42.0],"CRainRate":10.4,"ConvTypeMethod":1,"ConvType":2,"SeverityIntensity":0,"ConvTypeQuality":1,"MvtDirection":193,"CTPressure":10900.0,"NumIdCell":452,"CTReff":"","LonG":41.9944,"LatTrajCellCG":[-8.9993,-8.98,-8.91,-8.88],"ExpansionRate":96.696,"CTPhase":"","LatG":-8.9993,"BTmin":222.22,"BTmoy":244.3,"CTCot":"","CTCwp":"","NbPosLightning":0,"SeverityType":"","PhaseLife":4,"MvtSpeed":9.658,"Surface":1620.0,"Duration":2700,"CoolingRate":20.2824}},{"geometry":{"type":"Polygon","coordinates":[[[32.716,-8.879],[32.814,-8.795],[32.793,-8.623],[32.751,-8.566],[32.711,-8.537],[32.604,-8.535],[32.536,-8.562],[32.503,-8.59],[32.517,-8.704],[32.573,-8.876],[32.716,-8.879]]]},"type":"Feature","properties":{"NumIdBirth":499,"CType":12,"LonTrajCellCG":[32.6632,32.71,32.72,32.74,32.76,32.76,32.81],"CRainRate":0.0,"ConvTypeMethod":1,"ConvType":2,"SeverityIntensity":0,"ConvTypeQuality":1,"MvtDirection":343,"CTPressure":16900.0,"NumIdCell":457,"CTReff":"","LonG":32.6632,"LatTrajCellCG":[-8.7017,-8.79,-8.98,-9.17,-9.16,-9.19,-9.21],"ExpansionRate":-93.8879,"CTPhase":"","LatG":-8.7017,"BTmin":219.57,"BTmoy":253.36,"CTCot":"","CTCwp":"","NbPosLightning":0,"SeverityType":"","PhaseLife":3,"MvtSpeed":8.11,"Surface":690.0,"Duration":5400,"CoolingRate":-1.5336}},{"geometry":{"type":"Polygon","coordinates":[[[30.273,-9.295],[30.471,-9.212],[30.502,-9.184],[30.53,-9.128],[30.558,-9.072],[30.549,-8.986],[30.47,-8.9],[30.398,-8.87],[30.329,-8.869],[30.294,-8.869],[30.107,-9.036],[30.129,-9.235],[30.17,-9.293],[30.273,-9.295]]]},"type":"Feature","properties":{"NumIdBirth":528,"CType":8,"LonTrajCellCG":[30.3327,30.4,30.37,30.4,30.41],"CRainRate":10.9,"ConvTypeMethod":1,"ConvType":2,"SeverityIntensity":3,"ConvTypeQuality":2,"MvtDirection":279,"CTPressure":10400.0,"NumIdCell":464,"CTReff":"","LonG":30.3327,"LatTrajCellCG":[-9.087,-9.05,-9.1,-9.1,-9.09],"ExpansionRate":1426.8242,"CTPhase":"","LatG":-9.087,"BTmin":211.09,"BTmoy":252.12,"CTCot":"","CTCwp":"","NbPosLightning":0,"SeverityType":"","PhaseLife":3,"MvtSpeed":4.232,"Surface":1120.0,"Duration":3600,"CoolingRate":-93.456}},{"geometry":{"type":"Polygon","coordinates":[[[32.972,-9.483],[33.041,-9.455],[33.069,-9.399],[33.051,-9.255],[32.906,-9.253],[32.878,-9.31],[32.9,-9.482],[32.972,-9.483]]]},"type":"Feature","properties":{"NumIdBirth":452,"CType":8,"LonTrajCellCG":[32.9713,33.0,33.05,33.04,33.02,33.02,33.04,32.89,33.15],"CRainRate":4.6,"ConvTypeMethod":1,"ConvType":2,"SeverityIntensity":2,"ConvTypeQuality":3,"MvtDirection":225,"CTPressure":13300.0,"NumIdCell":468,"CTReff":"","LonG":32.9713,"LatTrajCellCG":[-9.3589,-9.22,-9.26,-9.29,-9.26,-9.26,-9.25,-9.23,-9.22],"ExpansionRate":183.3121,"CTPhase":"","LatG":-9.3589,"BTmin":221.57,"BTmoy":237.65,"CTCot":"","CTCwp":"","NbPosLightning":0,"SeverityType":"","PhaseLife":3,"MvtSpeed":2.457,"Surface":265.0,"Duration":7200,"CoolingRate":-51.5232}},{"geometry":{"type":"Polygon","coordinates":[[[33.243,-9.345],[33.308,-9.288],[33.3,-9.231],[33.253,-9.145],[33.108,-9.142],[33.134,-9.343],[33.243,-9.345]]]},"type":"Feature","properties":{"NumIdBirth":522,"CType":8,"LonTrajCellCG":[33.1952,33.22,33.24],"CRainRate":0.0,"ConvTypeMethod":1,"ConvType":2,"SeverityIntensity":2,"ConvTypeQuality":1,"MvtDirection":275,"CTPressure":22800.0,"NumIdCell":469,"CTReff":"","LonG":33.1952,"LatTrajCellCG":[-9.2405,-9.25,-9.25],"ExpansionRate":1821.528,"CTPhase":"","LatG":-9.2405,"BTmin":231.47,"BTmoy":242.22,"CTCot":"","CTCwp":"","NbPosLightning":0,"SeverityType":"","PhaseLife":3,"MvtSpeed":1.782,"Surface":230.0,"Duration":1800,"CoolingRate":-49.9392}},{"geometry":{"type":"Polygon","coordinates":[[[41.618,-10.146],[41.693,-10.089],[41.725,-10.032],[41.751,-9.945],[41.734,-9.857],[41.679,-9.798],[41.582,-9.737],[41.453,-9.735],[41.421,-9.792],[41.417,-9.996],[41.445,-10.142],[41.618,-10.146]]]},"type":"Feature","properties":{"NumIdBirth":459,"CType":8,"LonTrajCellCG":[41.5663,41.55,41.57,41.62,41.69,41.66,41.67,41.7,41.7],"CRainRate":45.9,"ConvTypeMethod":1,"ConvType":2,"SeverityIntensity":2,"ConvTypeQuality":3,"MvtDirection":246,"CTPressure":11100.0,"NumIdCell":475,"CTReff":"","LonG":41.5663,"LatTrajCellCG":[-9.9449,-9.97,-9.96,-9.9,-9.7,-9.94,-9.9,-9.88,-9.87],"ExpansionRate":576.144,"CTPhase":"","LatG":-9.9449,"BTmin":200.79,"BTmoy":216.16,"CTCot":"","CTCwp":"","NbPosLightning":0,"SeverityType":"","PhaseLife":3,"MvtSpeed":4.846,"Surface":925.0,"Duration":7200,"CoolingRate":-44.2368}},{"geometry":{"type":"Polygon","coordinates":[[[41.391,-10.083],[41.428,-10.054],[41.411,-9.967],[41.362,-9.936],[41.277,-9.935],[41.239,-9.963],[41.262,-10.08],[41.391,-10.083]]]},"type":"Feature","properties":{"NumIdBirth":0,"CType":8,"LonTrajCellCG":[41.3297],"CRainRate":11.1,"ConvTypeMethod":1,"ConvType":2,"SeverityIntensity":1,"ConvTypeQuality":3,"MvtDirection":245,"CTPressure":17100.0,"NumIdCell":476,"CTReff":"","LonG":41.3297,"LatTrajCellCG":[-10.0134],"ExpansionRate":"","CTPhase":"","LatG":-10.0134,"BTmin":214.95,"BTmoy":221.11,"CTCot":"","CTCwp":"","NbPosLightning":0,"SeverityType":"","PhaseLife":1,"MvtSpeed":4.826,"Surface":140.0,"Duration":0,"CoolingRate":-44.2368}},{"geometry":{"type":"Polygon","coordinates":[[[41.265,-10.313],[41.247,-10.225],[41.193,-10.166],[41.15,-10.165],[41.107,-10.164],[41.136,-10.31],[41.265,-10.313]]]},"type":"Feature","properties":{"NumIdBirth":404,"CType":8,"LonTrajCellCG":[41.1806,41.24,41.3,41.33,41.34,41.37,41.4,41.4,41.41,41.4,41.4],"CRainRate":0.5,"ConvTypeMethod":1,"ConvType":2,"SeverityIntensity":0,"ConvTypeQuality":1,"MvtDirection":232,"CTPressure":22200.0,"NumIdCell":477,"CTReff":"","LonG":41.1806,"LatTrajCellCG":[-10.2481,-10.19,-10.1,-9.99,-9.79,-9.71,-9.63,-9.52,-9.43,-9.41,-9.41],"ExpansionRate":-89.712,"CTPhase":"","LatG":-10.2481,"BTmin":233.1,"BTmoy":237.15,"CTCot":"","CTCwp":"","NbPosLightning":0,"SeverityType":"","PhaseLife":4,"MvtSpeed":10.251,"Surface":95.0,"Duration":9000,"CoolingRate":12.1968}},{"geometry":{"type":"Polygon","coordinates":[[[41.769,-10.471],[41.844,-10.414],[41.821,-10.296],[41.771,-10.266],[41.641,-10.263],[41.615,-10.35],[41.627,-10.409],[41.683,-10.469],[41.769,-10.471]]]},"type":"Feature","properties":{"NumIdBirth":0,"CType":8,"LonTrajCellCG":[41.7326],"CRainRate":11.8,"ConvTypeMethod":1,"ConvType":2,"SeverityIntensity":2,"ConvTypeQuality":1,"MvtDirection":262,"CTPressure":11700.0,"NumIdCell":478,"CTReff":"","LonG":41.7326,"LatTrajCellCG":[-10.3607],"ExpansionRate":"","CTPhase":"","LatG":-10.3607,"BTmin":212.67,"BTmoy":228.41,"CTCot":"","CTCwp":"","NbPosLightning":0,"SeverityType":"","PhaseLife":1,"MvtSpeed":4.03,"Surface":235.0,"Duration":0,"CoolingRate":-66.3336}},{"geometry":{"type":"Polygon","coordinates":[[[41.775,-10.704],[41.812,-10.676],[41.837,-10.589],[41.819,-10.501],[41.769,-10.471],[41.683,-10.469],[41.639,-10.468],[41.688,-10.702],[41.775,-10.704]]]},"type":"Feature","properties":{"NumIdBirth":530,"CType":8,"LonTrajCellCG":[41.7332,41.75,41.79,41.85,41.87,41.9],"CRainRate":0.4,"ConvTypeMethod":1,"ConvType":2,"SeverityIntensity":2,"ConvTypeQuality":1,"MvtDirection":262,"CTPressure":22700.0,"NumIdCell":479,"CTReff":"","LonG":41.7332,"LatTrajCellCG":[-10.5749,-10.49,-10.45,-10.39,-10.32,-10.26],"ExpansionRate":-197.3519,"CTPhase":"","LatG":-10.5749,"BTmin":231.47,"BTmoy":237.77,"CTCot":"","CTCwp":"","NbPosLightning":0,"SeverityType":"","PhaseLife":2,"MvtSpeed":4.032,"Surface":235.0,"Duration":4500,"CoolingRate":-66.3336}},{"geometry":{"type":"Polygon","coordinates":[[[42.373,-11.246],[42.486,-11.161],[42.63,-11.018],[42.578,-10.782],[42.207,-10.481],[42.119,-10.479],[42.056,-10.594],[42.037,-10.711],[42.03,-10.886],[42.056,-11.004],[42.132,-11.152],[42.189,-11.212],[42.24,-11.243],[42.373,-11.246]]]},"type":"Feature","properties":{"NumIdBirth":512,"CType":8,"LonTrajCellCG":[42.2985,42.29,42.29,42.29,42.27,42.3,42.31],"CRainRate":15.5,"ConvTypeMethod":1,"ConvType":2,"SeverityIntensity":0,"ConvTypeQuality":1,"MvtDirection":197,"CTPressure":10800.0,"NumIdCell":482,"CTReff":"","LonG":42.2985,"LatTrajCellCG":[-10.8983,-10.8,-10.74,-10.7,-10.68,-10.65,-10.62],"ExpansionRate":5.8321,"CTPhase":"","LatG":-10.8983,"BTmin":210.28,"BTmoy":229.82,"CTCot":"","CTCwp":"","NbPosLightning":0,"SeverityType":"","PhaseLife":4,"MvtSpeed":9.252,"Surface":2840.0,"Duration":5400,"CoolingRate":15.3}},{"geometry":{"type":"Polygon","coordinates":[[[42.202,-11.653],[42.277,-11.596],[42.338,-11.48],[42.291,-11.274],[42.233,-11.213],[41.962,-10.972],[41.874,-10.97],[41.85,-11.057],[41.814,-11.291],[41.834,-11.38],[41.918,-11.558],[42.07,-11.65],[42.202,-11.653]]]},"type":"Feature","properties":{"NumIdBirth":470,"CType":8,"LonTrajCellCG":[42.0665,42.07,42.07,42.07,42.06,42.04,42.08,42.12,42.15],"CRainRate":16.6,"ConvTypeMethod":1,"ConvType":2,"SeverityIntensity":0,"ConvTypeQuality":3,"MvtDirection":208,"CTPressure":12500.0,"NumIdCell":483,"CTReff":"","LonG":42.0665,"LatTrajCellCG":[-11.3589,-11.26,-11.18,-11.12,-11.07,-11.07,-11.13,-11.15,-11.14],"ExpansionRate":4.896,"CTPhase":"","LatG":-11.3589,"BTmin":213.05,"BTmoy":230.94,"CTCot":"","CTCwp":"","NbPosLightning":0,"SeverityType":"","PhaseLife":4,"MvtSpeed":9.67,"Surface":2044.9999,"Duration":7200,"CoolingRate":8.0208}},{"geometry":{"type":"Polygon","coordinates":[[[29.267,-9.762],[29.298,-9.734],[29.359,-9.678],[29.418,-9.594],[29.411,-9.537],[29.34,-9.507],[29.238,-9.505],[29.075,-9.56],[29.044,-9.587],[29.017,-9.644],[29.023,-9.701],[29.06,-9.73],[29.131,-9.76],[29.267,-9.762]]]},"type":"Feature","properties":{"NumIdBirth":535,"CType":6,"LonTrajCellCG":[29.2116,29.26,29.32,29.33],"CRainRate":0.0,"ConvTypeMethod":1,"ConvType":2,"SeverityIntensity":3,"ConvTypeQuality":3,"MvtDirection":268,"CTPressure":20200.0,"NumIdCell":487,"CTReff":"","LonG":29.2116,"LatTrajCellCG":[-9.634,-9.61,-9.6,-9.57],"ExpansionRate":1282.464,"CTPhase":"","LatG":-9.634,"BTmin":233.37,"BTmoy":263.49,"CTCot":"","CTCwp":"","NbPosLightning":0,"SeverityType":"","PhaseLife":2,"MvtSpeed":5.792,"Surface":570.0,"Duration":2700,"CoolingRate":-77.5296}},{"geometry":{"type":"Polygon","coordinates":[[[39.266,-9.892],[39.302,-9.864],[39.281,-9.747],[39.118,-9.744],[39.143,-9.889],[39.266,-9.892]]]},"type":"Feature","properties":{"NumIdBirth":527,"CType":8,"LonTrajCellCG":[39.2076,39.09,39.1,39.13,39.17,39.17],"CRainRate":7.8,"ConvTypeMethod":1,"ConvType":2,"SeverityIntensity":1,"ConvTypeQuality":1,"MvtDirection":322,"CTPressure":10900.0,"NumIdCell":489,"CTReff":"","LonG":39.2076,"LatTrajCellCG":[-9.8143,-9.99,-9.98,-9.97,-9.95,-9.95],"ExpansionRate":-192.816,"CTPhase":"","LatG":-9.8143,"BTmin":213.05,"BTmoy":220.88,"CTCot":"","CTCwp":"","NbPosLightning":0,"SeverityType":"","PhaseLife":2,"MvtSpeed":1.11,"Surface":160.0,"Duration":4500,"CoolingRate":-24.2424}},{"geometry":{"type":"Polygon","coordinates":[[[39.154,-10.18],[39.189,-10.151],[39.22,-10.094],[39.184,-9.89],[39.021,-9.887],[38.949,-9.943],[38.99,-10.176],[39.154,-10.18]]]},"type":"Feature","properties":{"NumIdBirth":0,"CType":8,"LonTrajCellCG":[39.0845],"CRainRate":16.9,"ConvTypeMethod":1,"ConvType":2,"SeverityIntensity":1,"ConvTypeQuality":1,"MvtDirection":322,"CTPressure":11300.0,"NumIdCell":490,"CTReff":"","LonG":39.0845,"LatTrajCellCG":[-10.0315],"ExpansionRate":"","CTPhase":"","LatG":-10.0315,"BTmin":200.79,"BTmoy":217.73,"CTCot":"","CTCwp":"","NbPosLightning":0,"SeverityType":"","PhaseLife":1,"MvtSpeed":1.109,"Surface":560.0,"Duration":0,"CoolingRate":-24.2424}},{"geometry":{"type":"Polygon","coordinates":[[[39.089,-10.498],[39.125,-10.469],[39.104,-10.353],[39.052,-10.293],[38.971,-10.292],[38.935,-10.32],[38.962,-10.466],[39.008,-10.496],[39.089,-10.498]]]},"type":"Feature","properties":{"NumIdBirth":553,"CType":8,"LonTrajCellCG":[39.0273,39.05,39.09],"CRainRate":13.2,"ConvTypeMethod":1,"ConvType":2,"SeverityIntensity":1,"ConvTypeQuality":2,"MvtDirection":340,"CTPressure":16800.0,"NumIdCell":491,"CTReff":"","LonG":39.0273,"LatTrajCellCG":[-10.3979,-10.43,-10.44],"ExpansionRate":1122.1199,"CTPhase":"","LatG":-10.3979,"BTmin":218.19,"BTmoy":221.03,"CTCot":"","CTCwp":"","NbPosLightning":0,"SeverityType":"","PhaseLife":3,"MvtSpeed":4.374,"Surface":190.0,"Duration":1800,"CoolingRate":1.3536}},{"geometry":{"type":"Polygon","coordinates":[[[39.128,-10.702],[39.163,-10.674],[39.264,-10.56],[39.248,-10.472],[39.125,-10.469],[39.054,-10.526],[38.989,-10.612],[38.999,-10.67],[39.046,-10.7],[39.128,-10.702]]]},"type":"Feature","properties":{"NumIdBirth":497,"CType":8,"LonTrajCellCG":[39.1296,39.11,39.1,39.08,39.1,39.1,39.11,39.1],"CRainRate":12.1,"ConvTypeMethod":1,"ConvType":2,"SeverityIntensity":1,"ConvTypeQuality":2,"MvtDirection":62,"CTPressure":14500.0,"NumIdCell":492,"CTReff":"","LonG":39.1296,"LatTrajCellCG":[-10.5818,-10.59,-10.58,-10.6,-10.65,-10.65,-10.63,-10.59],"ExpansionRate":501.4081,"CTPhase":"","LatG":-10.5818,"BTmin":216.77,"BTmoy":221.25,"CTCot":"","CTCwp":"","NbPosLightning":0,"SeverityType":"","PhaseLife":2,"MvtSpeed":1.821,"Surface":220.0,"Duration":6300,"CoolingRate":-5.508}},{"geometry":{"type":"Polygon","coordinates":[[[39.169,-11.928],[39.398,-11.846],[39.503,-11.761],[39.533,-11.703],[39.562,-11.645],[39.585,-11.558],[39.567,-11.47],[39.483,-11.264],[39.096,-10.963],[39.049,-10.933],[39.009,-10.932],[38.968,-10.931],[38.892,-10.958],[38.751,-11.072],[38.803,-11.335],[38.951,-11.864],[38.998,-11.895],[39.086,-11.926],[39.169,-11.928]]]},"type":"Feature","properties":{"NumIdBirth":529,"CType":8,"LonTrajCellCG":[39.157,39.45,39.46,39.25,39.54,39.55,39.54],"CRainRate":14.3,"ConvTypeMethod":1,"ConvType":2,"SeverityIntensity":0,"ConvTypeQuality":1,"MvtDirection":240,"CTPressure":12400.0,"NumIdCell":493,"CTReff":"","LonG":39.157,"LatTrajCellCG":[-11.448,-11.54,-11.56,-11.42,-11.64,-11.66,-11.61],"ExpansionRate":"","CTPhase":"","LatG":-11.448,"BTmin":211.09,"BTmoy":241.77,"CTCot":"","CTCwp":"","NbPosLightning":0,"SeverityType":"","PhaseLife":3,"MvtSpeed":6.255,"Surface":5245.0002,"Duration":5400,"CoolingRate":3.3408}},{"geometry":{"type":"Polygon","coordinates":[[[29.41,-10.106],[29.505,-10.051],[29.492,-9.936],[29.454,-9.907],[29.352,-9.905],[29.321,-9.933],[29.294,-9.99],[29.308,-10.104],[29.41,-10.106]]]},"type":"Feature","properties":{"NumIdBirth":539,"CType":8,"LonTrajCellCG":[29.396,29.32,29.45,29.45],"CRainRate":3.6,"ConvTypeMethod":1,"ConvType":2,"SeverityIntensity":2,"ConvTypeQuality":3,"MvtDirection":315,"CTPressure":16900.0,"NumIdCell":497,"CTReff":"","LonG":29.396,"LatTrajCellCG":[-10.0045,-10.11,-10.04,-10.05],"ExpansionRate":753.264,"CTPhase":"","LatG":-10.0045,"BTmin":223.82,"BTmoy":240.18,"CTCot":"","CTCwp":"","NbPosLightning":0,"SeverityType":"","PhaseLife":3,"MvtSpeed":4.455,"Surface":240.0,"Duration":2700,"CoolingRate":-24.9336}},{"geometry":{"type":"Polygon","coordinates":[[[36.104,-10.347],[36.172,-10.291],[36.148,-10.146],[36.072,-10.144],[36.033,-10.144],[35.932,-10.228],[35.951,-10.344],[36.104,-10.347]]]},"type":"Feature","properties":{"NumIdBirth":555,"CType":7,"LonTrajCellCG":[36.056,36.06,36.08],"CRainRate":0.0,"ConvTypeMethod":1,"ConvType":2,"SeverityIntensity":1,"ConvTypeQuality":2,"MvtDirection":338,"CTPressure":23400.0,"NumIdCell":500,"CTReff":"","LonG":36.056,"LatTrajCellCG":[-10.2513,-10.29,-10.29],"ExpansionRate":297.288,"CTPhase":"","LatG":-10.2513,"BTmin":242.34,"BTmoy":257.76,"CTCot":"","CTCwp":"","NbPosLightning":0,"SeverityType":"","PhaseLife":2,"MvtSpeed":3.253,"Surface":285.0,"Duration":1800,"CoolingRate":-35.64}},{"geometry":{"type":"Polygon","coordinates":[[[33.347,-10.608],[33.415,-10.581],[33.517,-10.525],[33.683,-10.414],[33.748,-10.357],[33.727,-10.213],[33.677,-10.126],[33.203,-10.117],[33.138,-10.173],[33.107,-10.46],[33.124,-10.575],[33.201,-10.605],[33.347,-10.608]]]},"type":"Feature","properties":{"NumIdBirth":551,"CType":8,"LonTrajCellCG":[33.3861,33.46,33.53],"CRainRate":17.1,"ConvTypeMethod":1,"ConvType":2,"SeverityIntensity":1,"ConvTypeQuality":1,"MvtDirection":341,"CTPressure":10100.0,"NumIdCell":502,"CTReff":"","LonG":33.3861,"LatTrajCellCG":[-10.3297,-10.4,-10.48],"ExpansionRate":29.0881,"CTPhase":"","LatG":-10.3297,"BTmin":199.77,"BTmoy":224.59,"CTCot":"","CTCwp":"","NbPosLightning":0,"SeverityType":"","PhaseLife":2,"MvtSpeed":8.478,"Surface":2195.0001,"Duration":1800,"CoolingRate":-30.312}},{"geometry":{"type":"Polygon","coordinates":[[[33.399,-10.955],[33.431,-10.927],[33.45,-10.812],[33.424,-10.639],[33.383,-10.609],[33.237,-10.606],[33.205,-10.634],[33.24,-10.865],[33.285,-10.924],[33.326,-10.953],[33.399,-10.955]]]},"type":"Feature","properties":{"NumIdBirth":462,"CType":9,"LonTrajCellCG":[33.3301,33.39,33.47,33.55,33.59,33.61,33.71,33.71,33.71],"CRainRate":13.8,"ConvTypeMethod":1,"ConvType":2,"SeverityIntensity":2,"ConvTypeQuality":1,"MvtDirection":318,"CTPressure":10200.0,"NumIdCell":503,"CTReff":"","LonG":33.3301,"LatTrajCellCG":[-10.7625,-10.78,-10.8,-10.58,-10.62,-10.64,-10.7,-10.75,-10.78],"ExpansionRate":1793.16,"CTPhase":"","LatG":-10.7625,"BTmin":204.18,"BTmoy":219.68,"CTCot":"","CTCwp":"","NbPosLightning":0,"SeverityType":"","PhaseLife":2,"MvtSpeed":7.633,"Surface":555.0,"Duration":7200,"CoolingRate":-21.0888}},{"geometry":{"type":"Polygon","coordinates":[[[33.61,-11.132],[33.642,-11.104],[33.633,-11.046],[33.592,-11.016],[33.481,-11.014],[33.495,-11.101],[33.536,-11.131],[33.61,-11.132]]]},"type":"Feature","properties":{"NumIdBirth":562,"CType":6,"LonTrajCellCG":[33.557,33.56],"CRainRate":0.0,"ConvTypeMethod":1,"ConvType":1,"SeverityIntensity":0,"ConvTypeQuality":3,"MvtDirection":324,"CTPressure":47000.0,"NumIdCell":505,"CTReff":"","LonG":33.557,"LatTrajCellCG":[-11.0685,-11.07],"ExpansionRate":201.6,"CTPhase":"","LatG":-11.0685,"BTmin":263.98,"BTmoy":269.22,"CTCot":"","CTCwp":"","NbPosLightning":0,"SeverityType":"","PhaseLife":2,"MvtSpeed":6.257,"Surface":80.0,"Duration":900,"CoolingRate":-12.2904}},{"geometry":{"type":"Polygon","coordinates":[[[31.901,-10.293],[31.874,-10.092],[31.834,-10.063],[31.799,-10.062],[31.763,-10.062],[31.7,-10.118],[31.719,-10.261],[31.794,-10.291],[31.901,-10.293]]]},"type":"Feature","properties":{"NumIdBirth":544,"CType":6,"LonTrajCellCG":[31.7917,31.83],"CRainRate":0.0,"ConvTypeMethod":1,"ConvType":1,"SeverityIntensity":0,"ConvTypeQuality":1,"MvtDirection":302,"CTPressure":43800.0,"NumIdCell":507,"CTReff":"","LonG":31.7917,"LatTrajCellCG":[-10.1802,-10.17],"ExpansionRate":397.584,"CTPhase":"","LatG":-10.1802,"BTmin":260.94,"BTmoy":272.32,"CTCot":"","CTCwp":"","NbPosLightning":0,"SeverityType":"","PhaseLife":2,"MvtSpeed":4.131,"Surface":210.0,"Duration":900,"CoolingRate":-14.4216}},{"geometry":{"type":"Polygon","coordinates":[[[36.935,-11.35],[37.003,-11.293],[36.992,-11.235],[36.797,-10.796],[36.689,-10.619],[36.572,-10.617],[36.398,-10.729],[36.384,-10.873],[36.399,-10.961],[36.645,-11.256],[36.734,-11.316],[36.817,-11.347],[36.935,-11.35]]]},"type":"Feature","properties":{"NumIdBirth":502,"CType":8,"LonTrajCellCG":[36.6741,36.75,36.81,36.84,36.87,36.93,36.98,37.07],"CRainRate":13.4,"ConvTypeMethod":1,"ConvType":2,"SeverityIntensity":0,"ConvTypeQuality":1,"MvtDirection":333,"CTPressure":16400.0,"NumIdCell":510,"CTReff":"","LonG":36.6741,"LatTrajCellCG":[-10.9774,-11.03,-11.02,-11.05,-11.14,-11.07,-11.07,-11.01],"ExpansionRate":103.2481,"CTPhase":"","LatG":-10.9774,"BTmin":213.44,"BTmoy":240.83,"CTCot":"","CTCwp":"","NbPosLightning":0,"SeverityType":"","PhaseLife":2,"MvtSpeed":5.605,"Surface":2220.0,"Duration":6300,"CoolingRate":-7.6464}},{"geometry":{"type":"Polygon","coordinates":[[[35.397,-10.679],[35.431,-10.651],[35.403,-10.477],[35.36,-10.447],[35.247,-10.445],[35.27,-10.59],[35.322,-10.677],[35.397,-10.679]]]},"type":"Feature","properties":{"NumIdBirth":467,"CType":8,"LonTrajCellCG":[35.3413,35.37,35.39,35.46,35.39,35.38,35.37,35.36,35.36],"CRainRate":8.8,"ConvTypeMethod":1,"ConvType":2,"SeverityIntensity":1,"ConvTypeQuality":1,"MvtDirection":8,"CTPressure":16100.0,"NumIdCell":511,"CTReff":"","LonG":35.3413,"LatTrajCellCG":[-10.5515,-10.64,-10.69,-10.73,-10.74,-10.78,-10.77,-10.79,-10.82],"ExpansionRate":443.088,"CTPhase":"","LatG":-10.5515,"BTmin":220.91,"BTmoy":239.2,"CTCot":"","CTCwp":"","NbPosLightning":0,"SeverityType":"","PhaseLife":3,"MvtSpeed":7.703,"Surface":215.0,"Duration":7200,"CoolingRate":-18.9648}},{"geometry":{"type":"Polygon","coordinates":[[[35.208,-10.675],[35.241,-10.647],[35.265,-10.561],[35.251,-10.474],[35.176,-10.472],[35.105,-10.5],[35.072,-10.528],[35.09,-10.644],[35.132,-10.673],[35.208,-10.675]]]},"type":"Feature","properties":{"NumIdBirth":559,"CType":8,"LonTrajCellCG":[35.1675,35.2,35.23],"CRainRate":3.1,"ConvTypeMethod":1,"ConvType":2,"SeverityIntensity":3,"ConvTypeQuality":3,"MvtDirection":305,"CTPressure":23000.0,"NumIdCell":512,"CTReff":"","LonG":35.1675,"LatTrajCellCG":[-10.5731,-10.6,-10.66],"ExpansionRate":1481.328,"CTPhase":"","LatG":-10.5731,"BTmin":233.37,"BTmoy":243.07,"CTCot":"","CTCwp":"","NbPosLightning":0,"SeverityType":"","PhaseLife":2,"MvtSpeed":5.308,"Surface":190.0,"Duration":1800,"CoolingRate":-56.7288}},{"geometry":{"type":"Polygon","coordinates":[[[36.302,-10.843],[36.336,-10.814],[36.316,-10.698],[36.214,-10.552],[36.061,-10.548],[36.003,-10.663],[36.013,-10.721],[36.186,-10.84],[36.302,-10.843]]]},"type":"Feature","properties":{"NumIdBirth":551,"CType":7,"LonTrajCellCG":[36.1774,36.24,36.17,36.16],"CRainRate":0.0,"ConvTypeMethod":1,"ConvType":2,"SeverityIntensity":2,"ConvTypeQuality":2,"MvtDirection":40,"CTPressure":20900.0,"NumIdCell":516,"CTReff":"","LonG":36.1774,"LatTrajCellCG":[-10.6945,-10.79,-10.77,-10.74],"ExpansionRate":755.7121,"CTPhase":"","LatG":-10.6945,"BTmin":242.11,"BTmoy":258.08,"CTCot":"","CTCwp":"","NbPosLightning":0,"SeverityType":"","PhaseLife":2,"MvtSpeed":3.373,"Surface":470.0,"Duration":2700,"CoolingRate":-30.1752}},{"geometry":{"type":"Polygon","coordinates":[[[42.47,-12.22],[42.434,-12.071],[42.368,-11.981],[42.309,-11.921],[41.975,-11.618],[41.844,-11.615],[41.794,-11.966],[41.844,-12.174],[41.895,-12.204],[42.47,-12.22]]]},"type":"Feature","properties":{"NumIdBirth":423,"CType":8,"LonTrajCellCG":[42.0973,42.15,42.19,42.22,42.55,42.58,42.56,42.57,42.59,42.61,42.6],"CRainRate":2.0,"ConvTypeMethod":1,"ConvType":2,"SeverityIntensity":0,"ConvTypeQuality":1,"MvtDirection":230,"CTPressure":17500.0,"NumIdCell":526,"CTReff":"","LonG":42.0973,"LatTrajCellCG":[-12.0263,-11.99,-11.93,-11.86,-11.76,-11.68,-11.51,-11.44,-11.4,-11.39,-11.38],"ExpansionRate":-63.8639,"CTPhase":"","LatG":-12.0263,"BTmin":220.24,"BTmoy":245.12,"CTCot":"","CTCwp":"","NbPosLightning":0,"SeverityType":"","PhaseLife":4,"MvtSpeed":8.812,"Surface":2420.0,"Duration":9000,"CoolingRate":18.3672}},{"geometry":{"type":"Polygon","coordinates":[[[30.609,-11.848],[30.697,-11.734],[30.685,-11.648],[30.475,-11.643],[30.5,-11.817],[30.539,-11.846],[30.609,-11.848]]]},"type":"Feature","properties":{"NumIdBirth":572,"CType":6,"LonTrajCellCG":[30.5711,30.57],"CRainRate":0.0,"ConvTypeMethod":1,"ConvType":1,"SeverityIntensity":2,"ConvTypeQuality":3,"MvtDirection":344,"CTPressure":22700.0,"NumIdCell":531,"CTReff":"","LonG":30.5711,"LatTrajCellCG":[-11.721,-11.74],"ExpansionRate":1553.2562,"CTPhase":"","LatG":-11.721,"BTmin":262.92,"BTmoy":271.87,"CTCot":"","CTCwp":"","NbPosLightning":0,"SeverityType":"","PhaseLife":2,"MvtSpeed":3.903,"Surface":200.0,"Duration":900,"CoolingRate":-35.7264}}]} \ No newline at end of file diff --git a/forest/test_cli.py b/test/test_cli.py similarity index 90% rename from forest/test_cli.py rename to test/test_cli.py index 8cdc80b58..d841b4966 100644 --- a/forest/test_cli.py +++ b/test/test_cli.py @@ -1,8 +1,15 @@ import unittest import forest.cli.main +from forest.cli.main import parse_args class TestForestCLI(unittest.TestCase): + def test_files(self): + paths = ["file.json"] + result = getattr(parse_args(paths), "files") + expect = paths + self.assertEqual(expect, result) + def test_parse_args(self): namespace = forest.cli.main.parse_args(["file.nc"]) result = namespace.files diff --git a/forest/test_config.py b/test/test_config.py similarity index 97% rename from forest/test_config.py rename to test/test_config.py index 2acf1214a..a60df0658 100644 --- a/forest/test_config.py +++ b/test/test_config.py @@ -6,7 +6,7 @@ class TestIntegration(unittest.TestCase): def setUp(self): - path = os.path.join(os.path.dirname(__file__), 'config.yaml') + path = os.path.join(os.path.dirname(__file__), '../forest/config.yaml') self.config = forest.load_config(path) def test_load_server_config_first_group(self): diff --git a/forest/test_data.py b/test/test_data.py similarity index 98% rename from forest/test_data.py rename to test/test_data.py index 0d7eded00..ddca839bc 100644 --- a/forest/test_data.py +++ b/test/test_data.py @@ -3,9 +3,10 @@ import datetime as dt import netCDF4 import numpy as np -import forest.satellite as satellite -import forest.data as data -import forest.db as db +from forest import ( + satellite, + data, + db) @unittest.skip("green light") diff --git a/test/test_db_control.py b/test/test_db_control.py new file mode 100644 index 000000000..d9a8bc5bd --- /dev/null +++ b/test/test_db_control.py @@ -0,0 +1,540 @@ +import unittest +import unittest.mock +import datetime as dt +import numpy as np +from forest import db + + +def test_convert_datetime64_array_to_strings(): + times = np.array( + [dt.datetime(2019, 1, 1), dt.datetime(2019, 1, 2)], + dtype="datetime64[s]") + result = db.stamps(times) + expect = ["2019-01-01 00:00:00", "2019-01-02 00:00:00"] + assert expect == result + + +def test_type_system_middleware(): + times = np.array( + [dt.datetime(2019, 1, 1), dt.datetime(2019, 1, 2)], + dtype="datetime64[s]") + converter = db.Converter({"valid_times": db.stamps}) + store = db.Store(db.reducer, middlewares=[converter]) + store.dispatch(db.set_value("valid_times", times)) + result = store.state + expect = { + "valid_times": ["2019-01-01 00:00:00", "2019-01-02 00:00:00"] + } + assert expect == result + + +class TestDatabaseMiddleware(unittest.TestCase): + def setUp(self): + self.database = db.Database.connect(":memory:") + self.controls = db.Controls(self.database) + self.store = db.Store(db.reducer, middlewares=[self.controls]) + + def tearDown(self): + self.database.close() + + def test_state_change_given_dropdown_message(self): + action = db.set_value("pressure", "1000") + self.store.dispatch(action) + result = self.store.state + expect = {"pressure": 1000.} + self.assertEqual(expect, result) + + def test_set_initial_time(self): + initial = dt.datetime(2019, 1, 1) + valid = dt.datetime(2019, 1, 1, 3) + self.database.insert_file_name("file.nc", initial) + self.database.insert_time("file.nc", "variable", valid, 0) + action = db.set_value("initial_time", "2019-01-01 00:00:00") + initial_state={ + "pattern": "file.nc", + "variable": "variable"} + store = db.Store( + db.reducer, + initial_state=initial_state, + middlewares=[self.controls]) + store.dispatch(action) + result = store.state + expect = { + "pattern": "file.nc", + "variable": "variable", + "initial_time": str(initial), + "valid_times": [str(valid)] + } + self.assertEqual(expect, result) + + def test_set_pattern(self): + initial = dt.datetime(2019, 1, 1) + valid = dt.datetime(2019, 1, 1, 3) + self.database.insert_file_name("file.nc", initial) + self.database.insert_time("file.nc", "variable", valid, 0) + action = db.set_value("pattern", "file.nc") + self.store.dispatch(action) + result = self.store.state + expect = { + "pattern": "file.nc", + "variables": ["variable"], + "initial_times": [str(initial)] + } + self.assertEqual(expect, result) + + @unittest.skip("porting to test directory") + def test_set_valid_time_sets_pressures(self): + path = "file.nc" + variable = "air_temperature" + initial_time = "2019-01-01 00:00:00" + valid_time = "2019-01-01 12:00:00" + pressure = 750. + index = 0 + self.database.insert_file_name(path, initial_time) + self.database.insert_time(path, variable, valid_time, index) + self.database.insert_pressure(path, variable, pressure, index) + store = db.Store( + db.reducer, + middlewares=[self.controls]) + actions = [ + db.set_value("pattern", path), + db.set_value("variable", variable), + db.set_value("initial_time", initial_time), + db.set_value("valid_time", valid_time)] + for action in actions: + store.dispatch(action) + result = store.state + expect = { + "pattern": path, + "variable": variable, + "variables": [variable], + "initial_time": initial_time, + "initial_times": [initial_time], + "valid_time": valid_time, + "valid_times": [valid_time], + "pressures": [pressure], + } + self.assertEqual(expect, result) + + @unittest.skip("porting to test directory") + def test_set_variable_given_initial_time_changes_times_and_pressure(self): + path = "some.nc" + initial_time = "2019-01-01 00:00:00" + self.database.insert_file_name(path, initial_time) + index = 0 + for variable, valid_time, pressure in [ + ("air_temperature", "2019-01-01 01:00:00", 1000.), + ("olr", "2019-01-01 01:03:00", 10.)]: + self.database.insert_time(path, variable, valid_time, index) + self.database.insert_pressure(path, variable, pressure, index) + + store = db.Store( + db.reducer, + middlewares=[self.controls]) + actions = [ + db.set_value("pattern", path), + db.set_value("variable", "air_temperature"), + db.set_value("initial_time", initial_time), + db.set_value("variable", "olr") + ] + for action in actions: + store.dispatch(action) + + result = store.state + expect = { + "pattern": path, + "variable": "olr", + "variables": ["air_temperature", "olr"], + "initial_time": initial_time, + "initial_times": [initial_time], + "valid_times": ["2019-01-01 01:03:00"], + "pressures": [0.0] + } + self.assertEqual(expect, result) + + @unittest.skip("porting to test directory") + def test_navigator_api(self): + path = "file.nc" + variable = "air_temperature" + initial_time = "2019-01-01 00:00:00" + valid_time = "2019-01-01 12:00:00" + pressure = 750. + navigator = db.Navigator() + controls = db.Controls(navigator) + store = db.Store( + db.reducer, + middlewares=[controls]) + actions = [ + db.set_value("pattern", path), + db.set_value("variable", variable), + db.set_value("initial_time", initial_time), + db.set_value("valid_time", valid_time)] + for action in actions: + store.dispatch(action) + result = store.state + expect = { + "pattern": path, + "variable": variable, + "variables": [variable], + "initial_time": initial_time, + "initial_times": [initial_time], + "valid_time": valid_time, + "valid_times": [valid_time], + "pressures": [pressure], + } + self.assertEqual(expect, result) + + +class TestControls(unittest.TestCase): + def setUp(self): + self.database = db.Database.connect(":memory:") + + def tearDown(self): + self.database.close() + + def test_on_variable_emits_state(self): + value = "token" + listener = unittest.mock.Mock() + view = db.ControlView() + view.subscribe(listener) + view.on_change("variable")(None, None, value) + listener.assert_called_once_with(db.set_value("variable", value)) + + def test_next_pressure_given_pressures_returns_first_element(self): + pressure = 950 + store = db.Store( + db.reducer, + initial_state={"pressures": [pressure]}, + middlewares=[ + db.next_previous, + db.Controls(self.database)]) + view = db.ControlView() + view.subscribe(store.dispatch) + view.on_next('pressure', 'pressures')() + result = store.state + expect = { + "pressure": pressure, + "pressures": [pressure] + } + self.assertEqual(expect, result) + + def test_next_pressure_given_pressures_none(self): + store = db.Store( + db.reducer, + middlewares=[ + db.InverseCoordinate("pressure"), + db.next_previous, + db.Controls(self.database) + ]) + view = db.ControlView() + view.subscribe(store.dispatch) + view.on_next('pressure', 'pressures')() + result = store.state + expect = {} + self.assertEqual(expect, result) + + def test_next_pressure_given_current_pressure(self): + pressure = 950 + pressures = [1000, 950, 800] + store = db.Store( + db.reducer, + initial_state={ + "pressure": pressure, + "pressures": pressures + }, + middlewares=[ + db.InverseCoordinate("pressure"), + db.next_previous, + db.Controls(self.database) + ]) + view = db.ControlView() + view.subscribe(store.dispatch) + view.on_next('pressure', 'pressures')() + result = store.state["pressure"] + expect = 800 + self.assertEqual(expect, result) + + +class TestControlView(unittest.TestCase): + def setUp(self): + self.view = db.ControlView() + + def test_on_click_emits_action(self): + listener = unittest.mock.Mock() + self.view.subscribe(listener) + self.view.on_next("pressure", "pressures")() + expect = db.next_value("pressure", "pressures") + listener.assert_called_once_with(expect) + + def test_render_given_no_variables_disables_dropdown(self): + self.view.render({"variables": []}) + result = self.view.dropdowns["variable"].disabled + expect = True + self.assertEqual(expect, result) + + def test_render_given_pressure(self): + state = { + "pressures": [1000], + "pressure": 1000 + } + self.view.render(state) + result = self.view.dropdowns["pressure"].label + expect = "1000hPa" + self.assertEqual(expect, result) + + def test_render_sets_pressure_levels(self): + pressures = [1000, 950, 850] + self.view.render({"pressures": pressures}) + result = self.view.dropdowns["pressure"].menu + expect = ["1000hPa", "950hPa", "850hPa"] + self.assert_label_equal(expect, result) + + def test_render_given_initial_time_populates_valid_time_menu(self): + state = {"valid_times": [dt.datetime(2019, 1, 1, 3)]} + self.view.render(state) + result = self.view.dropdowns["valid_time"].menu + expect = ["2019-01-01 03:00:00"] + self.assert_label_equal(expect, result) + + def test_render_state_configures_variable_menu(self): + self.view.render({"variables": ["mslp"]}) + result = self.view.dropdowns["variable"].menu + expect = ["mslp"] + self.assert_label_equal(expect, result) + self.assert_value_equal(expect, result) + + def test_render_state_configures_initial_time_menu(self): + initial_times = ["2019-01-01 12:00:00", "2019-01-01 00:00:00"] + state = {"initial_times": initial_times} + self.view.render(state) + result = self.view.dropdowns["initial_time"].menu + expect = initial_times + self.assert_label_equal(expect, result) + + def assert_label_equal(self, expect, result): + result = [l for l, _ in result] + self.assertEqual(expect, result) + + def assert_value_equal(self, expect, result): + result = [v for _, v in result] + self.assertEqual(expect, result) + + def test_render_initial_times_disables_buttons(self): + key = "initial_time" + self.view.render({}) + self.assertEqual(self.view.dropdowns[key].disabled, True) + self.assertEqual(self.view.buttons[key]["next"].disabled, True) + self.assertEqual(self.view.buttons[key]["previous"].disabled, True) + + def test_hpa_given_small_pressures(self): + result = db.ControlView.hpa(0.001) + expect = "0.001hPa" + self.assertEqual(expect, result) + + def test_render_variables_given_null_state_disables_dropdown(self): + self.view.render({}) + result = self.view.dropdowns["variable"].disabled + expect = True + self.assertEqual(expect, result) + + def test_render_initial_times_enables_buttons(self): + key = "initial_time" + self.view.render({"initial_times": ["2019-01-01 00:00:00"]}) + self.assertEqual(self.view.dropdowns[key].disabled, False) + self.assertEqual(self.view.buttons[key]["next"].disabled, False) + self.assertEqual(self.view.buttons[key]["previous"].disabled, False) + + def test_render_valid_times_given_null_state_disables_buttons(self): + self.check_disabled("valid_time", {}, True) + + def test_render_valid_times_given_empty_list_disables_buttons(self): + self.check_disabled("valid_time", {"valid_times": []}, True) + + def test_render_valid_times_given_values_enables_buttons(self): + state = {"valid_times": ["2019-01-01 00:00:00"]} + self.check_disabled("valid_time", state, False) + + def test_render_pressures_given_null_state_disables_buttons(self): + self.check_disabled("pressure", {}, True) + + def test_render_pressures_given_empty_list_disables_buttons(self): + self.check_disabled("pressure", {"pressures": []}, True) + + def test_render_pressures_given_values_enables_buttons(self): + self.check_disabled("pressure", {"pressures": [1000.00000001]}, False) + + def check_disabled(self, key, state, expect): + self.view.render(state) + self.assertEqual(self.view.dropdowns[key].disabled, expect) + self.assertEqual(self.view.buttons[key]["next"].disabled, expect) + self.assertEqual(self.view.buttons[key]["previous"].disabled, expect) + + +class TestNextPrevious(unittest.TestCase): + def setUp(self): + self.initial_times = [ + "2019-01-02 00:00:00", + "2019-01-01 00:00:00", + "2019-01-04 00:00:00", + "2019-01-03 00:00:00", + ] + + def test_middleware_converts_next_value_to_set_value(self): + log = db.Log() + state = { + "k": 2, + "ks": [1, 2, 3] + } + store = db.Store( + db.reducer, + initial_state=state, + middlewares=[ + db.next_previous, + log]) + store.dispatch(db.next_value("k", "ks")) + result = store.state + expect = { + "k": 3, + "ks": [1, 2, 3] + } + self.assertEqual(expect, result) + self.assertEqual(log.actions, [db.set_value("k", 3)]) + + def test_next_value_action_creator(self): + result = db.next_value("initial_time", "initial_times") + expect = { + "kind": "NEXT_VALUE", + "payload": { + "item_key": "initial_time", + "items_key": "initial_times" + } + } + self.assertEqual(expect, result) + + def test_previous_value_action_creator(self): + result = db.previous_value("initial_time", "initial_times") + expect = { + "kind": "PREVIOUS_VALUE", + "payload": { + "item_key": "initial_time", + "items_key": "initial_times" + } + } + self.assertEqual(expect, result) + + def test_set_value_action_creator(self): + result = db.set_value("K", "V") + expect = { + "kind": "SET_VALUE", + "payload": dict(key="K", value="V") + } + self.assertEqual(expect, result) + + def test_next_given_none_selects_latest_time(self): + action = db.next_value("initial_time", "initial_times") + state = dict(initial_times=self.initial_times) + store = db.Store( + db.reducer, + initial_state=state, + middlewares=[db.next_previous]) + store.dispatch(action) + result = store.state + expect = dict( + initial_time="2019-01-04 00:00:00", + initial_times=self.initial_times) + self.assertEqual(expect, result) + + def test_reducer_given_set_value_action_adds_key_value(self): + action = db.set_value("name", "value") + state = {"previous": "constant"} + result = db.reducer(state, action) + expect = { + "previous": "constant", + "name": "value" + } + self.assertEqual(expect, result) + + def test_reducer_next_given_time_moves_forward_in_time(self): + initial_times = [ + "2019-01-01 00:00:00", + "2019-01-01 02:00:00", + "2019-01-01 01:00:00" + ] + action = db.next_value("initial_time", "initial_times") + state = { + "initial_time": "2019-01-01 00:00:00", + "initial_times": initial_times + } + store = db.Store( + db.reducer, + initial_state=state, + middlewares=[db.next_previous]) + store.dispatch(action) + result = store.state + expect = { + "initial_time": "2019-01-01 01:00:00", + "initial_times": initial_times + } + self.assertEqual(expect, result) + + def test_previous_given_none_selects_earliest_time(self): + action = db.previous_value("initial_time", "initial_times") + state = {"initial_times": self.initial_times} + store = db.Store( + db.reducer, + initial_state=state, + middlewares=[db.next_previous]) + store.dispatch(action) + result = store.state + expect = { + "initial_time": "2019-01-01 00:00:00", + "initial_times": self.initial_times + } + self.assertEqual(expect, result) + + def test_next_item_given_last_item_returns_first_item(self): + result = db.control.next_item([0, 1, 2], 2) + expect = 0 + self.assertEqual(expect, result) + + def test_previous_item_given_first_item_returns_last_item(self): + result = db.control.previous_item([0, 1, 2], 0) + expect = 2 + self.assertEqual(expect, result) + + +class TestPressureMiddleware(unittest.TestCase): + def test_pressure_middleware_reverses_pressure_coordinate(self): + pressures = [1000, 850, 500] + state = { + "pressure": 850, + "pressures": pressures + } + store = db.Store( + db.reducer, + initial_state=state, + middlewares=[ + db.InverseCoordinate("pressure"), + db.next_previous]) + action = db.next_value("pressure", "pressures") + store.dispatch(action) + result = store.state + expect = { + "pressure": 500, + "pressures": pressures + } + self.assertEqual(expect, result) + + +class TestStateStream(unittest.TestCase): + def test_support_old_style_state(self): + """Not all components are ready to accept dict() states""" + listener = unittest.mock.Mock() + store = db.Store(db.reducer) + old_states = (db.Stream() + .listen_to(store) + .map(lambda x: db.State(**x))) + old_states.subscribe(listener) + store.dispatch(db.set_value("pressure", 1000)) + expect = db.State(pressure=1000) + listener.assert_called_once_with(expect) diff --git a/forest/test_db_current.py b/test/test_db_current.py similarity index 99% rename from forest/test_db_current.py rename to test/test_db_current.py index 66fd84268..ac07d0d85 100644 --- a/forest/test_db_current.py +++ b/test/test_db_current.py @@ -1,7 +1,7 @@ import unittest import datetime as dt import sqlite3 -import db +from forest import db class TestDatabase(unittest.TestCase): diff --git a/forest/test_db_future.py b/test/test_db_future.py similarity index 99% rename from forest/test_db_future.py rename to test/test_db_future.py index ec007ac23..dddf73e38 100644 --- a/forest/test_db_future.py +++ b/test/test_db_future.py @@ -3,7 +3,7 @@ import netCDF4 import datetime as dt import sqlite3 -import db.future as db +import forest.db.future as db class TestDatabase(unittest.TestCase): diff --git a/forest/test_db_locate.py b/test/test_db_locate.py similarity index 97% rename from forest/test_db_locate.py rename to test/test_db_locate.py index 047dc61b0..09fde6f4c 100644 --- a/forest/test_db_locate.py +++ b/test/test_db_locate.py @@ -1,7 +1,8 @@ import unittest import sqlite3 import datetime as dt -import db +from forest import db +from forest.exceptions import SearchFail class TestLocate(unittest.TestCase): @@ -49,7 +50,7 @@ def test_locate_given_data_not_in_database_raises_exception(self): initial_time = dt.datetime(2019, 1, 1, 0) valid_time = dt.datetime(2019, 1, 1, 2) - with self.assertRaises(db.SearchFail): + with self.assertRaises(SearchFail): self.locator.locate(pattern, variable, initial_time, valid_time) diff --git a/forest/test_db_main.py b/test/test_db_main.py similarity index 99% rename from forest/test_db_main.py rename to test/test_db_main.py index 8a6bcf0f5..6d7b43b12 100644 --- a/forest/test_db_main.py +++ b/test/test_db_main.py @@ -3,7 +3,8 @@ import os import sqlite3 import netCDF4 -import db.main as main +import forest.db.main as main +import forest class TestMain(unittest.TestCase): diff --git a/test/test_disk.py b/test/test_disk.py new file mode 100644 index 000000000..f3b858f6d --- /dev/null +++ b/test/test_disk.py @@ -0,0 +1,399 @@ +import unittest +import datetime as dt +import numpy as np +import netCDF4 +import os +import fnmatch +import pytest +from forest import ( + disk, + navigate, + unified_model, + tutorial) +from forest.exceptions import SearchFail + + +class TestLocatorScalability(unittest.TestCase): + def test_locate_given_one_thousand_files(self): + N = 10**4 + k = np.random.randint(N) + start = dt.datetime(2019, 1, 10) + times = [start + dt.timedelta(hours=i) for i in range(N)] + paths = ["test-locate-{:%Y%m%dT%H%MZ}.nc".format(time) for time in times] + locator = unified_model.Locator(paths) + result = locator.find_paths(times[k]) + expect = [paths[k]] + self.assertEqual(expect, result) + + +class TestLocator(unittest.TestCase): + def setUp(self): + self.path = "test-navigator.nc" + + def tearDown(self): + if os.path.exists(self.path): + os.remove(self.path) + + def test_locator_given_empty_file_raises_exception(self): + pattern = self.path + with netCDF4.Dataset(self.path, "w") as dataset: + pass + variable = "relative_humidity" + initial_time = dt.datetime(2019, 1, 1) + valid_time = dt.datetime(2019, 1, 1) + locator = unified_model.Locator([self.path]) + with self.assertRaises(SearchFail): + locator.locate( + pattern, + variable, + initial_time, + valid_time) + + def test_locator_given_dim0_format(self): + pattern = self.path + times = [dt.datetime(2019, 1, 1), dt.datetime(2019, 1, 2)] + with netCDF4.Dataset(self.path, "w") as dataset: + um = tutorial.UM(dataset) + dataset.createDimension("longitude", 1) + dataset.createDimension("latitude", 1) + var = um.times("time", length=len(times), dim_name="dim0") + var[:] = netCDF4.date2num(times, units=var.units) + um.forecast_reference_time(times[0]) + var = um.pressures("pressure", length=len(times), dim_name="dim0") + var[:] = 1000. + dims = ("dim0", "longitude", "latitude") + coordinates = "forecast_period_1 forecast_reference_time pressure time" + var = um.relative_humidity(dims, coordinates=coordinates) + var[:] = 100. + variable = "relative_humidity" + initial_time = dt.datetime(2019, 1, 1) + valid_time = dt.datetime(2019, 1, 2) + locator = unified_model.Locator([self.path]) + _, result = locator.locate( + pattern, + variable, + initial_time, + valid_time, + pressure=1000.0001) + expect = (1,) + self.assertEqual(expect, result) + + def test_locator_given_time_pressure_format(self): + pattern = self.path + reference_time = dt.datetime(2019, 1, 1) + times = [dt.datetime(2019, 1, 2), dt.datetime(2019, 1, 2, 3)] + pressures = [1000, 950, 850] + with netCDF4.Dataset(self.path, "w") as dataset: + um = tutorial.UM(dataset) + dataset.createDimension("longitude", 1) + dataset.createDimension("latitude", 1) + var = um.times("time", length=len(times)) + var[:] = netCDF4.date2num(times, units=var.units) + um.forecast_reference_time(reference_time) + var = um.pressures("pressure", length=len(pressures)) + var[:] = pressures + dims = ("time", "pressure", "longitude", "latitude") + coordinates = "forecast_period_1 forecast_reference_time" + var = um.relative_humidity(dims, coordinates=coordinates) + var[:] = 100. + variable = "relative_humidity" + initial_time = reference_time + valid_time = times[1] + pressure = pressures[2] + locator = unified_model.Locator([self.path]) + _, result = locator.locate( + pattern, + variable, + initial_time, + valid_time, + pressure) + expect = (1, 2) + self.assertEqual(expect, result) + + def test_locator_given_time_outside_time_axis(self): + pattern = self.path + reference_time = dt.datetime(2019, 1, 1) + times = [dt.datetime(2019, 1, 2), dt.datetime(2019, 1, 2, 3)] + future = dt.datetime(2019, 1, 4) + pressures = [1000, 950, 850] + with netCDF4.Dataset(self.path, "w") as dataset: + um = tutorial.UM(dataset) + dataset.createDimension("longitude", 1) + dataset.createDimension("latitude", 1) + var = um.times("time", length=len(times)) + var[:] = netCDF4.date2num(times, units=var.units) + um.forecast_reference_time(reference_time) + var = um.pressures("pressure", length=len(pressures)) + var[:] = pressures + dims = ("time", "pressure", "longitude", "latitude") + coordinates = "forecast_period_1 forecast_reference_time" + var = um.relative_humidity(dims, coordinates=coordinates) + var[:] = 100. + variable = "relative_humidity" + initial_time = reference_time + valid_time = future + pressure = pressures[2] + locator = unified_model.Locator([self.path]) + with self.assertRaises(SearchFail): + locator.locate( + pattern, + variable, + initial_time, + valid_time, + pressure) + + def test_initial_time_given_forecast_reference_time(self): + time = dt.datetime(2019, 1, 1, 12) + with netCDF4.Dataset(self.path, "w") as dataset: + um = tutorial.UM(dataset) + um.forecast_reference_time(time) + coords = unified_model.Coordinates() + result = coords.initial_time(self.path) + expect = time + np.testing.assert_array_equal(expect, result) + + def test_valid_times(self): + units = "hours since 1970-01-01 00:00:00" + times = { + "time_0": [dt.datetime(2019, 1, 1)], + "time_1": [dt.datetime(2019, 1, 1, 3)]} + with netCDF4.Dataset(self.path, "w") as dataset: + um = tutorial.UM(dataset) + for name, values in times.items(): + var = um.times(name, length=len(values)) + var[:] = netCDF4.date2num(values, units=var.units) + var = um.pressures("pressure", length=1) + var[:] = 1000. + var = um.longitudes(length=1) + var[:] = 125. + var = um.latitudes(length=1) + var[:] = 45. + dims = ("time_1", "pressure", "longitude", "latitude") + var = um.relative_humidity(dims) + var[:] = 100. + variable = "relative_humidity" + coord = unified_model.Coordinates() + result = coord.valid_times(self.path, variable) + expect = times["time_1"] + np.testing.assert_array_equal(expect, result) + + def test_pressure_axis_given_time_pressure_lon_lat_dimensions(self): + with netCDF4.Dataset(self.path, "w") as dataset: + um = tutorial.UM(dataset) + dims = ("time_1", "pressure_0", "longitude", "latitude") + for dim in dims: + dataset.createDimension(dim, 1) + var = um.relative_humidity(dims) + result = disk.pressure_axis(self.path, "relative_humidity") + expect = 1 + self.assertEqual(expect, result) + + def test_pressure_axis_given_dim0_format(self): + coordinates = "forecast_period_1 forecast_reference_time pressure time" + with netCDF4.Dataset(self.path, "w") as dataset: + um = tutorial.UM(dataset) + dims = ("dim0", "longitude", "latitude") + for dim in dims: + dataset.createDimension(dim, 1) + var = um.relative_humidity(dims, coordinates=coordinates) + result = disk.pressure_axis(self.path, "relative_humidity") + expect = 0 + self.assertEqual(expect, result) + + def test_time_axis_given_time_pressure_lon_lat_dimensions(self): + with netCDF4.Dataset(self.path, "w") as dataset: + um = tutorial.UM(dataset) + dims = ("time_1", "pressure_0", "longitude", "latitude") + for dim in dims: + dataset.createDimension(dim, 1) + var = um.relative_humidity(dims) + result = disk.time_axis(self.path, "relative_humidity") + expect = 0 + self.assertEqual(expect, result) + + def test_time_axis_given_dim0_format(self): + coordinates = "forecast_period_1 forecast_reference_time pressure time" + with netCDF4.Dataset(self.path, "w") as dataset: + um = tutorial.UM(dataset) + dims = ("dim0", "longitude", "latitude") + for dim in dims: + dataset.createDimension(dim, 1) + var = um.relative_humidity(dims, coordinates=coordinates) + result = disk.time_axis(self.path, "relative_humidity") + expect = 0 + self.assertEqual(expect, result) + + +class TestNavigator(unittest.TestCase): + def setUp(self): + self.path = "test-navigator.nc" + + def tearDown(self): + if os.path.exists(self.path): + os.remove(self.path) + + def test_given_empty_unified_model_file(self): + pattern = "*.nc" + with netCDF4.Dataset(self.path, "w") as dataset: + pass + navigator = navigate.FileSystem.file_type([self.path], "unified_model") + result = navigator.variables(pattern) + expect = [] + self.assertEqual(expect, result) + + def test_initial_times_given_forecast_reference_time(self): + pattern = "*.nc" + with netCDF4.Dataset(self.path, "w") as dataset: + var = dataset.createVariable("forecast_reference_time", "d", ()) + var.units = "hours since 1970-01-01 00:00:00" + var[:] = 0 + navigator = navigate.FileSystem.file_type([self.path], "unified_model") + result = navigator.initial_times(pattern) + expect = [dt.datetime(1970, 1, 1)] + self.assertEqual(expect, result) + + def test_valid_times_given_relative_humidity(self): + pattern = "*.nc" + variable = "relative_humidity" + initial_time = dt.datetime(2019, 1, 1) + valid_times = [ + dt.datetime(2019, 1, 1, 0), + dt.datetime(2019, 1, 1, 1), + dt.datetime(2019, 1, 1, 2) + ] + pressures = [1000., 1000., 1000.] + units = "hours since 1970-01-01 00:00:00" + with netCDF4.Dataset(self.path, "w") as dataset: + # Dimensions + dataset.createDimension("dim0", 3) + dataset.createDimension("longitude", 1) + dataset.createDimension("latitude", 1) + + # Forecast reference time + var = dataset.createVariable("forecast_reference_time", "d", ()) + var.units = units + var[:] = netCDF4.date2num(initial_time, units=units) + + # Time + var = dataset.createVariable("time", "d", ("dim0",)) + var.units = units + var[:] = netCDF4.date2num(valid_times, units=units) + + # Pressure + var = dataset.createVariable("pressure", "d", ("dim0",)) + var[:] = pressures + + # Relative humidity + var = dataset.createVariable( + variable, "f", ("dim0", "longitude", "latitude")) + var[:] = 100. + var.standard_name = "relative_humidity" + var.units = "%" + var.um_stash_source = "m01s16i256" + var.grid_mapping = "longitude_latitude" + var.coordinates = "forecast_period forecast_reference_time time" + + navigator = navigate.FileSystem([self.path]) + result = navigator.valid_times(pattern, variable, initial_time) + expect = valid_times + np.testing.assert_array_equal(expect, result) + + def test_pressures_given_relative_humidity(self): + pattern = "*.nc" + variable = "relative_humidity" + initial_time = dt.datetime(2019, 1, 1) + valid_times = [ + dt.datetime(2019, 1, 1, 0), + dt.datetime(2019, 1, 1, 1), + dt.datetime(2019, 1, 1, 2) + ] + pressures = [1000., 1000., 1000.] + units = "hours since 1970-01-01 00:00:00" + with netCDF4.Dataset(self.path, "w") as dataset: + # Dimensions + dataset.createDimension("pressure", 3) + dataset.createDimension("longitude", 1) + dataset.createDimension("latitude", 1) + + # Forecast reference time + var = dataset.createVariable("forecast_reference_time", "d", ()) + var.units = units + var[:] = netCDF4.date2num(initial_time, units=units) + + # Time + var = dataset.createVariable("time", "d", ("pressure",)) + var.units = units + var[:] = netCDF4.date2num(valid_times, units=units) + + # Pressure + var = dataset.createVariable("pressure", "d", ("pressure",)) + var[:] = pressures + + # Relative humidity + var = dataset.createVariable( + variable, "f", ("pressure", "longitude", "latitude")) + var[:] = 100. + var.standard_name = "relative_humidity" + var.units = "%" + var.um_stash_source = "m01s16i256" + var.grid_mapping = "longitude_latitude" + var.coordinates = "forecast_period forecast_reference_time time" + + navigator = navigate.FileSystem([self.path]) + result = navigator.pressures(pattern, variable, initial_time) + expect = [1000.] + np.testing.assert_array_equal(expect, result) + + +def test_ndindex_given_dim0_format(): + times = [ + dt.datetime(2019, 1, 1, 0), + dt.datetime(2019, 1, 1, 3), + dt.datetime(2019, 1, 1, 6), + dt.datetime(2019, 1, 1, 0), + dt.datetime(2019, 1, 1, 3), + dt.datetime(2019, 1, 1, 6), + ] + pressures = [ + 1000.0001, + 1000.0001, + 1000.0001, + 0.0001, + 0.0001, + 0.0001, + ] + time = times[1] + pressure = pressures[1] + masks = [ + disk.time_mask(times, time), + disk.pressure_mask(pressures, pressure)] + axes = [0, 0] + result = disk.ndindex(masks, axes) + expect = (1,) + np.testing.assert_array_equal(expect, result) + + +class TestFNMatch(unittest.TestCase): + def test_filter(self): + names = ["/some/file.json", "/other/file.nc"] + result = fnmatch.filter(names, "*.nc") + expect = ["/other/file.nc"] + self.assertEqual(expect, result) + + +class TestNumpy(unittest.TestCase): + def test_concatenate(self): + arrays = [ + np.array([1, 2, 3]), + np.array([4, 5]) + ] + result = np.concatenate(arrays) + expect = np.array([1, 2, 3, 4, 5]) + np.testing.assert_array_equal(expect, result) + + def test_fancy_indexing(self): + x = np.zeros((3, 5, 2, 2)) + pts = (0, 2) + result = x[pts].shape + expect = (2, 2) + self.assertEqual(expect, result) diff --git a/forest/test_earth_networks.py b/test/test_earth_networks.py similarity index 96% rename from forest/test_earth_networks.py rename to test/test_earth_networks.py index a13827ef9..585c7d1c0 100644 --- a/forest/test_earth_networks.py +++ b/test/test_earth_networks.py @@ -1,7 +1,7 @@ import unittest import datetime as dt import glob -import earth_networks +from forest import earth_networks import pandas as pd import pandas.testing as pdt diff --git a/test/test_eida50.py b/test/test_eida50.py new file mode 100644 index 000000000..6e4213afd --- /dev/null +++ b/test/test_eida50.py @@ -0,0 +1,196 @@ +import unittest +import os +import datetime as dt +import netCDF4 +import numpy as np +from forest import (eida50, satellite, navigate) +from forest.exceptions import FileNotFound, IndexNotFound + + +class TestLocator(unittest.TestCase): + def setUp(self): + self.paths = [] + self.pattern = "test-eida50*.nc" + self.locator = satellite.Locator(self.pattern) + + def tearDown(self): + for path in self.paths: + if os.path.exists(path): + os.remove(path) + + def test_parse_date(self): + path = "/some/file-20190101.nc" + result = self.locator.parse_date(path) + expect = dt.datetime(2019, 1, 1) + self.assertEqual(expect, result) + + def test_find_given_no_files_raises_notfound(self): + any_date = dt.datetime.now() + with self.assertRaises(FileNotFound): + self.locator.find(any_date) + + def test_find_given_a_single_file(self): + valid_date = dt.datetime(2019, 1, 1) + path = "test-eida50-20190101.nc" + self.paths.append(path) + + times = [valid_date] + with netCDF4.Dataset(path, "w") as dataset: + self.set_times(dataset, times) + + found_path, index = self.locator.find(valid_date) + self.assertEqual(found_path, path) + self.assertEqual(index, 0) + + def test_find_given_multiple_files(self): + dates = [ + dt.datetime(2019, 1, 1), + dt.datetime(2019, 1, 2), + dt.datetime(2019, 1, 3)] + for date in dates: + path = "test-eida50-{:%Y%m%d}.nc".format(date) + self.paths.append(path) + with netCDF4.Dataset(path, "w") as dataset: + self.set_times(dataset, [date]) + valid_date = dt.datetime(2019, 1, 2, 0, 14) + found_path, index = self.locator.find(valid_date) + expect_path = "test-eida50-20190102.nc" + self.assertEqual(found_path, expect_path) + self.assertEqual(index, 0) + + def test_find_index_given_valid_time(self): + time = dt.datetime(2019, 1, 1, 3, 31) + times = [ + dt.datetime(2019, 1, 1, 3, 0), + dt.datetime(2019, 1, 1, 3, 15), + dt.datetime(2019, 1, 1, 3, 30), + dt.datetime(2019, 1, 1, 3, 45), + dt.datetime(2019, 1, 1, 4, 0), + ] + freq = dt.timedelta(minutes=15) + result = self.locator.find_index(times, time, freq) + expect = 2 + self.assertEqual(expect, result) + + def test_find_index_outside_range_raises_exception(self): + time = dt.datetime(2019, 1, 4, 16) + times = [ + dt.datetime(2019, 1, 1, 3, 0), + dt.datetime(2019, 1, 1, 3, 15), + dt.datetime(2019, 1, 1, 3, 30), + dt.datetime(2019, 1, 1, 3, 45), + dt.datetime(2019, 1, 1, 4, 0), + ] + freq = dt.timedelta(minutes=15) + with self.assertRaises(IndexNotFound): + self.locator.find_index(times, time, freq) + + def set_times(self, dataset, times): + units = "seconds since 1970-01-01 00:00:00" + dataset.createDimension("time", len(times)) + var = dataset.createVariable("time", "d", ("time",)) + var.units = units + var[:] = netCDF4.date2num(times, units=units) + + +class Formatter(object): + def __init__(self, dataset): + self.dataset = dataset + + def define(self, times): + dataset = self.dataset + dataset.createDimension("time", len(times)) + dataset.createDimension("longitude", 1) + dataset.createDimension("latitude", 1) + units = "hours since 1970-01-01 00:00:00" + var = dataset.createVariable( + "time", "d", ("time",)) + var.axis = "T" + var.units = units + var.standard_name = "time" + var.calendar = "gregorian" + var[:] = netCDF4.date2num(times, units=units) + var = dataset.createVariable( + "longitude", "f", ("longitude",)) + var.axis = "X" + var.units = "degrees_east" + var.standard_name = "longitude" + var[:] = 0 + var = dataset.createVariable( + "latitude", "f", ("latitude",)) + var.axis = "Y" + var.units = "degrees_north" + var.standard_name = "latitude" + var[:] = 0 + var = dataset.createVariable( + "data", "f", ("time", "latitude", "longitude")) + var.standard_name = "toa_brightness_temperature" + var.long_name = "toa_brightness_temperature" + var.units = "K" + var[:] = 0 + + +class TestCoordinates(unittest.TestCase): + def setUp(self): + self.path = "test-navigate-eida50.nc" + + def tearDown(self): + if os.path.exists(self.path): + os.remove(self.path) + + def test_valid_times_given_eida50_toa_brightness_temperature(self): + times = [dt.datetime(2019, 1, 1)] + with netCDF4.Dataset(self.path, "w") as dataset: + writer = Formatter(dataset) + writer.define(times) + + coord = eida50.Coordinates() + result = coord.valid_times(self.path, "toa_brightness_temperature") + expect = times + self.assertEqual(expect, result) + + +class TestEIDA50(unittest.TestCase): + def setUp(self): + self.path = "test-navigate-eida50.nc" + self.navigator = navigate.FileSystem.file_type([self.path], "eida50") + self.times = [ + dt.datetime(2019, 1, 1, 0), + dt.datetime(2019, 1, 1, 0, 15), + dt.datetime(2019, 1, 1, 0, 30), + dt.datetime(2019, 1, 1, 0, 45), + ] + + def tearDown(self): + if os.path.exists(self.path): + os.remove(self.path) + + def test_initial_times(self): + with netCDF4.Dataset(self.path, "w") as dataset: + writer = Formatter(dataset) + writer.define(self.times) + result = self.navigator.initial_times(self.path) + expect = [self.times[0]] + self.assertEqual(expect, result) + + def test_valid_times(self): + with netCDF4.Dataset(self.path, "w") as dataset: + writer = Formatter(dataset) + writer.define(self.times) + result = self.navigator.valid_times( + self.path, + "toa_brightness_temperature", + self.times[0]) + expect = self.times + np.testing.assert_array_equal(expect, result) + + def test_pressures(self): + with netCDF4.Dataset(self.path, "w") as dataset: + writer = Formatter(dataset) + writer.define(self.times) + result = self.navigator.pressures( + self.path, + "toa_brightness_temperature", + self.times[0]) + expect = [] + np.testing.assert_array_equal(expect, result) diff --git a/forest/test_image_controls.py b/test/test_image_controls.py similarity index 97% rename from forest/test_image_controls.py rename to test/test_image_controls.py index 342e13c90..7a85f7a95 100644 --- a/forest/test_image_controls.py +++ b/test/test_image_controls.py @@ -1,5 +1,5 @@ import unittest -import images +from forest import images class TestControls(unittest.TestCase): diff --git a/test/test_integration.py b/test/test_integration.py new file mode 100644 index 000000000..d0a424567 --- /dev/null +++ b/test/test_integration.py @@ -0,0 +1,36 @@ +import unittest +import subprocess +import signal +import time +try: + from selenium import webdriver + from selenium.common.exceptions import WebDriverException + from selenium.webdriver.firefox.options import Options +except ImportError: + pass + + +@unittest.skip("integration test") +class TestIntegration(unittest.TestCase): + def setUp(self): + options = Options() + options.headless = True + self.driver = webdriver.Firefox(options=options) + + def tearDown(self): + self.driver.quit() + + def test_command_line_forest(self): + process = subprocess.Popen(["forest", "file.json"], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE) + time.sleep(5) + try: + self.driver.get("http://localhost:5006") + except WebDriverException as e: + print(e.msg) + process.terminate() + o, e = process.communicate() + print(o.decode()) + print(e.decode()) + self.assertTrue(False) diff --git a/forest/test_lakes.py b/test/test_lakes.py similarity index 94% rename from forest/test_lakes.py rename to test/test_lakes.py index 1348fe771..f978b59d6 100644 --- a/forest/test_lakes.py +++ b/test/test_lakes.py @@ -1,6 +1,6 @@ import unittest import cartopy -import data +from forest import data class TestLakes(unittest.TestCase): diff --git a/test/test_main.py b/test/test_main.py new file mode 100644 index 000000000..b537e0e73 --- /dev/null +++ b/test/test_main.py @@ -0,0 +1,9 @@ +import os +from forest.main import main + + +def test_main_given_rdt_files(tmp_path): + rdt_file = tmp_path / "file.json" + with rdt_file.open("w"): + pass + main(argv=["--file-type", "rdt", str(rdt_file)]) diff --git a/test/test_navigator.py b/test/test_navigator.py new file mode 100644 index 000000000..b5a3912f1 --- /dev/null +++ b/test/test_navigator.py @@ -0,0 +1,53 @@ +import pytest +import forest +import datetime as dt +import numpy as np + + +@pytest.mark.skip("use real unified model file") +def test_unified_model_navigator(): + paths = ["unified.nc"] + navigator = forest.navigate.FileSystem( + paths, + coordinates=forest.unified_model.Coordinates()) + result = navigator.initial_times("*.nc") + expect = [] + assert expect == result + + +@pytest.fixture +def rdt_navigator(): + paths = ["/some/file_201901010000.json"] + return forest.navigate.FileSystem.file_type(paths, "rdt") + + +def test_rdt_navigator_valid_times_given_single_file(rdt_navigator): + paths = ["/some/rdt_201901010000.json"] + navigator = forest.navigate.FileSystem.file_type(paths, "rdt") + actual = navigator.valid_times("*.json", None, None) + expected = ["2019-01-01 00:00:00"] + assert actual == expected + + +def test_rdt_navigator_valid_times_given_multiple_files(rdt_navigator): + paths = [ + "/some/rdt_201901011200.json", + "/some/rdt_201901011215.json", + "/some/rdt_201901011230.json" + ] + navigator = forest.navigate.FileSystem.file_type(paths, "rdt") + actual = navigator.valid_times(paths[1], None, None) + expected = ["2019-01-01 12:15:00"] + np.testing.assert_array_equal(actual, expected) + + +def test_rdt_navigator_variables(rdt_navigator): + assert rdt_navigator.variables("*.json") == ["RDT"] + + +def test_rdt_navigator_initial_times(rdt_navigator): + assert rdt_navigator.initial_times("*.json") == ["2019-01-01 00:00:00"] + + +def test_rdt_navigator_pressures(rdt_navigator): + assert rdt_navigator.pressures("*.json", None, None) == [] diff --git a/test/test_parse_args.py b/test/test_parse_args.py new file mode 100644 index 000000000..765d9e9ae --- /dev/null +++ b/test/test_parse_args.py @@ -0,0 +1,31 @@ +import unittest +from forest.parse_args import parse_args + + +class TestParseArgs(unittest.TestCase): + def test_directory_returns_none_by_default(self): + argv = [ + "--database", "file.db", + "--config-file", "file.yml"] + self.check(argv, "directory", None) + + def test_directory_returns_value(self): + argv = [ + "--directory", "/some", + "--database", "file.db", + "--config-file", "file.yml"] + self.check(argv, "directory", "/some") + + def test_files(self): + self.check(["file.json"], "files", ["file.json"]) + + def test_file_type(self): + self.check(["--file-type", "rdt", "file.json"], "file_type", "rdt") + + def test_no_files_or_config_raises_system_exit(self): + with self.assertRaises(SystemExit): + parse_args([]) + + def check(self, argv, attr, expect): + result = getattr(parse_args(argv), attr) + self.assertEqual(expect, result) diff --git a/forest/test_preselect.py b/test/test_preselect.py similarity index 84% rename from forest/test_preselect.py rename to test/test_preselect.py index 5e4d3e713..af677b7b8 100644 --- a/forest/test_preselect.py +++ b/test/test_preselect.py @@ -1,6 +1,7 @@ import unittest -import db -import images +from forest import ( + db, + images) class TestInitialState(unittest.TestCase): @@ -46,28 +47,28 @@ def test_initial_state_given_pattern(self): variable = "relative_humidity" self.database.insert_variable(path, variable) state = db.initial_state(self.database, pattern="*.nc") - result = state.variable + result = state["variable"] expect = variable self.assertEqual(expect, result) def test_initial_state(self): self.make_database() state = db.initial_state(self.database) - self.assertEqual(state.initial_times, [ + self.assertEqual(state["initial_times"], [ "2019-01-01 00:00:00", "2019-01-01 12:00:00", ]) - self.assertEqual(state.initial_time, "2019-01-01 12:00:00") - self.assertEqual(state.valid_times, [ + self.assertEqual(state["initial_time"], "2019-01-01 12:00:00") + self.assertEqual(state["valid_times"], [ "2019-01-01 12:00:02", "2019-01-01 13:00:02" ]) - self.assertEqual(state.valid_time, "2019-01-01 12:00:02") - self.assertEqual(state.pressures, [ + self.assertEqual(state["valid_time"], "2019-01-01 12:00:02") + self.assertEqual(state["pressures"], [ 1000, 950 ]) - self.assertEqual(state.pressure, 1000) + self.assertEqual(state["pressure"], 1000) class TestImageControls(unittest.TestCase): diff --git a/forest/test_rdt.py b/test/test_rdt.py similarity index 92% rename from forest/test_rdt.py rename to test/test_rdt.py index 8784e14b8..b72d5d0f5 100644 --- a/forest/test_rdt.py +++ b/test/test_rdt.py @@ -4,13 +4,15 @@ import glob import json import numpy as np -import rdt -import locate +from forest import ( + rdt, + locate) class TestLocator(unittest.TestCase): def setUp(self): - pattern = os.path.join(os.path.dirname(__file__), "sample/RDT*.json") + pattern = os.path.join(os.path.dirname(__file__), + "sample/RDT*.json") self.locator = rdt.Locator(pattern) def test_paths(self): diff --git a/forest/test_series.py b/test/test_series.py similarity index 99% rename from forest/test_series.py rename to test/test_series.py index 2da07116c..220982b89 100644 --- a/forest/test_series.py +++ b/test/test_series.py @@ -4,7 +4,7 @@ import numpy as np import numpy.testing as npt import datetime as dt -import data +from forest import data def variable_dim0( diff --git a/forest/test_time_controls.py b/test/test_time_controls.py similarity index 92% rename from forest/test_time_controls.py rename to test/test_time_controls.py index daf7aee15..217e66bf4 100644 --- a/forest/test_time_controls.py +++ b/test/test_time_controls.py @@ -1,6 +1,6 @@ import unittest import datetime as dt -import data +from forest import data class TestInitialTimes(unittest.TestCase): diff --git a/test/test_tutorial.py b/test/test_tutorial.py new file mode 100644 index 000000000..c6ebd6caf --- /dev/null +++ b/test/test_tutorial.py @@ -0,0 +1,84 @@ +""" +Sample data should be available to let users/developers build +extensions easily +""" +import os +import netCDF4 +import sqlite3 +import yaml +import forest + + +def test_parse_args_build_dir(): + args = forest.tutorial.main.parse_args(["build"]) + assert args.build_dir == "build" + + +def test_main_calls_build_all_with_build_dir(tmpdir): + build_dir = str(tmpdir) + forest.tutorial.main.main([build_dir]) + assert os.path.exists(os.path.join(build_dir, forest.tutorial.RDT_FILE)) + + +def test_rdt_file_name(): + assert forest.tutorial.RDT_FILE == "rdt_201904171245.json" + + +def test_build_rdt_copies_rdt_file_to_directory(tmpdir): + directory = str(tmpdir) + forest.tutorial.build_rdt(directory) + expect = os.path.join(directory, os.path.basename(forest.tutorial.RDT_FILE)) + assert os.path.exists(expect) + + +def test_build_all_makes_global_um_output(tmpdir): + build_dir = str(tmpdir) + forest.tutorial.build_all(build_dir) + path = os.path.join(build_dir, forest.tutorial.UM_FILE) + with netCDF4.Dataset(path) as dataset: + var = dataset.variables["relative_humidity"] + + +def test_build_all_makes_eida50_example(tmpdir): + build_dir = str(tmpdir) + forest.tutorial.build_all(build_dir) + path = os.path.join(build_dir, forest.tutorial.EIDA50_FILE) + with netCDF4.Dataset(path) as dataset: + var = dataset.variables["data"] + + +def test_build_all_makes_sample_database(tmpdir): + build_dir = str(tmpdir) + forest.tutorial.build_all(build_dir) + file_name = os.path.join(build_dir, forest.tutorial.DB_FILE) + assert os.path.exists(file_name) + + +def test_build_database_adds_sample_nc_to_file_table(tmpdir): + build_dir = str(tmpdir) + forest.tutorial.build_database(build_dir) + db_path = os.path.join(build_dir, forest.tutorial.DB_FILE) + um_path = os.path.join(build_dir, forest.tutorial.UM_FILE) + connection = sqlite3.connect(db_path) + cursor = connection.cursor() + cursor.execute("SELECT name FROM file") + result = cursor.fetchall() + expect = [(um_path,)] + assert expect == result + + +def test_build_all_builds_config_file(tmpdir): + build_dir = str(tmpdir) + forest.tutorial.build_all(build_dir) + path = os.path.join(build_dir, forest.tutorial.CFG_FILE) + with open(path) as stream: + result = yaml.load(stream) + expect = { + "files": [{ + "label": "Unified Model", + "pattern": "*" + forest.tutorial.UM_FILE, + "directory": build_dir, + "locator": "database" + }] + } + assert expect == result