Commit 645ab9e7 authored by Le Roux Erwan's avatar Le Roux Erwan
Browse files

[HYPERCUBE] add all the extended classes of hypercube visualizer. add test_hypercube. refactor.

parent e65dbdd7
No related merge requests found
Showing with 242 additions and 82 deletions
+242 -82
......@@ -21,17 +21,22 @@ class AbstractHypercubeVisualizer(object):
trend_test_class,
nb_data_reduced_for_speed=False,
save_to_file=False,
nb_top_likelihood_values=1,
last_starting_year=None):
last_starting_year=None,
verbose=True):
self.verbose = verbose
self.last_starting_year = last_starting_year
self.nb_data_for_fast_mode = 7 if nb_data_reduced_for_speed else None
if isinstance(nb_data_reduced_for_speed, bool):
self.nb_data_for_fast_mode = 7 if nb_data_reduced_for_speed else None
else:
assert isinstance(nb_data_reduced_for_speed, int)
self.nb_data_for_fast_mode = nb_data_reduced_for_speed
self.save_to_file = save_to_file
self.trend_test_class = trend_test_class
self.tuple_to_study_visualizer = tuple_to_study_visualizer # type: Dict[Tuple, StudyVisualizer]
print('Hypercube with parameters:')
print(self.last_starting_year)
print(self.trend_test_class)
# print(self.nb_data_for_fast_mode)
if self.verbose:
print('Hypercube with parameters:')
print('Starting year:', self.last_starting_year)
print('Trend test class:', get_display_name_from_object_type(self.trend_test_class))
# Main attributes defining the hypercube
......
......@@ -28,19 +28,23 @@ class AltitudeHypercubeVisualizer(AbstractHypercubeVisualizer):
return self.display_trend_type_to_style.keys()
@property
def nb_axes(self):
def nb_rows(self):
return 1
def trend_type_to_series(self, reduction_function):
def trend_type_to_series(self, reduction_function, isin_parameters=None):
# Map each trend type to its serie with percentages
# Define here all the trend type we might need in the results/displays
trend_types_to_process = list(self.display_trend_types) + [AbstractUnivariateTest.SIGNIFICATIVE_ALL_TREND]
return {trend_type: self.trend_type_reduction_series(reduction_function, trend_type)
return {trend_type: self.trend_type_reduction_series(reduction_function=reduction_function,
df_bool=self.df_bool(trend_type, isin_parameters).copy())
for trend_type in trend_types_to_process}
def trend_type_reduction_series(self, reduction_function, display_trend_type):
def df_bool(self, display_trend_type, isin_parameters=None):
return self.df_hypercube_trend_type.isin(AbstractUnivariateTest.get_real_trend_types(display_trend_type))
def trend_type_reduction_series(self, reduction_function, df_bool):
# Reduce df_bool df to a serie s_trend_type_percentage
s_trend_type_percentage = reduction_function(self.df_bool(display_trend_type))
s_trend_type_percentage = reduction_function(df_bool)
assert isinstance(s_trend_type_percentage, pd.Series)
assert not isinstance(s_trend_type_percentage.index, pd.MultiIndex)
s_trend_type_percentage *= 100
......@@ -52,9 +56,6 @@ class AltitudeHypercubeVisualizer(AbstractHypercubeVisualizer):
# series = [s_trend_type_percentage, s_trend_strength]
return series
def df_bool(self, display_trend_type):
return self.df_hypercube_trend_type.isin(AbstractUnivariateTest.get_real_trend_types(display_trend_type))
def subtitle_to_reduction_function(self, reduction_function, level=None, add_detailed_plot=False, subtitle=None):
def reduction_function_with_level(df_bool, **kwargs):
return reduction_function(df_bool, **kwargs) if level is None else reduction_function(df_bool, level,
......@@ -77,28 +78,32 @@ class AltitudeHypercubeVisualizer(AbstractHypercubeVisualizer):
if xlabel != 'starting years':
labels.remove('starting years')
common_txt = 'averaged on {}'.format(' & '.join(labels))
common_txt += 'with any starting year <= {}'.format(str(self.last_starting_year))
common_txt += ' with any starting year <= {}'.format(str(self.last_starting_year))
return common_txt
def visualize_trend_test_evolution(self, reduction_function, xlabel, xlabel_values, axes=None, marker='o',
subtitle=''):
subtitle='', isin_parameters=None,
show_or_save_to_file=True,
plot_title=None):
if axes is None:
fig, axes = plt.subplots(self.nb_axes, 1, figsize=self.study_visualizer.figsize)
if not isinstance(axes, np.ndarray):
axes = [axes]
axes = self.load_trend_test_evolution_axes(self.nb_rows)
else:
assert len(axes) == self.nb_rows
trend_type_to_series = self.trend_type_to_series(reduction_function)
trend_type_to_series = self.trend_type_to_series(reduction_function, isin_parameters)
for ax_idx, ax in enumerate(axes):
for display_trend_type in self.display_trend_types:
style = self.display_trend_type_to_style[display_trend_type]
percentages_values = trend_type_to_series[display_trend_type][ax_idx]
xlabel_values = list(percentages_values.index)
percentages_values = list(percentages_values.values)
ax.plot(xlabel_values, percentages_values, style + marker, label=display_trend_type)
if ax_idx == 0:
# Global information
ax.set_ylabel(self.get_title_plot(xlabel, ax_idx=0))
if xlabel != STARTING_YEARS_XLABEL:
ax.set_yticks(list(range(0, 101, 10)))
ax.set_yticks(list(range(0, 101, 20)))
else:
ax.set_ylabel(self.get_title_plot(xlabel, ax_idx=ax_idx))
......@@ -110,36 +115,46 @@ class AltitudeHypercubeVisualizer(AbstractHypercubeVisualizer):
ax.set_xlabel(xlabel)
ax.grid()
ax.legend()
ax.set_title(plot_title)
specific_title = 'Evolution of {} trends (significative or not) wrt to the {} with {}'.format(subtitle, xlabel,
self.trend_test_name)
specific_title += '\n ' + self.get_title_plot(xlabel)
# Figure title
specific_title += '\n'
trend_types = [AbstractUnivariateTest.ALL_TREND,
AbstractUnivariateTest.SIGNIFICATIVE_ALL_TREND,
AbstractUnivariateTest.SIGNIFICATIVE_POSITIVE_TREND,
AbstractUnivariateTest.SIGNIFICATIVE_NEGATIVE_TREND]
series = [trend_type_to_series[trend_type][0] for trend_type in trend_types]
percents = [serie.sum() if xlabel == STARTING_YEARS_XLABEL else serie.mean() for serie in series]
percents = [round(p) for p in percents]
specific_title += 'Total ' if xlabel == STARTING_YEARS_XLABEL else 'Mean '
specific_title += 'all trend {}, all significative trends: {} (+:{} -{})'.format(*percents)
# specific_title += '\n'
#
# trend_types = [AbstractUnivariateTest.ALL_TREND,
# AbstractUnivariateTest.SIGNIFICATIVE_ALL_TREND,
# AbstractUnivariateTest.SIGNIFICATIVE_POSITIVE_TREND,
# AbstractUnivariateTest.SIGNIFICATIVE_NEGATIVE_TREND]
# series = [trend_type_to_series[trend_type][0] for trend_type in trend_types]
# percents = [serie.sum() if xlabel == STARTING_YEARS_XLABEL else serie.mean() for serie in series]
# percents = [np.round(p) for p in percents]
# specific_title += 'Total ' if xlabel == STARTING_YEARS_XLABEL else 'Mean '
# specific_title += 'all trend {}, all significative trends: {} (+:{} -{})'.format(*percents)
plt.suptitle(specific_title)
self.show_or_save_to_file(specific_title=specific_title)
if show_or_save_to_file:
self.show_or_save_to_file(specific_title=specific_title)
return specific_title
def load_trend_test_evolution_axes(self, nb_rows):
fig, axes = plt.subplots(nb_rows, 1, figsize=self.study_visualizer.figsize)
if not isinstance(axes, np.ndarray):
axes = [axes]
return axes
def visualize_trend_test_repartition(self, reduction_function, axes=None, subtitle=''):
def visualize_trend_test_repartition(self, reduction_function, axes=None, subtitle='', isin_parameters=None):
if axes is None:
nb_trend_type = len(self.display_trend_type_to_style)
fig, axes = plt.subplots(self.nb_axes, nb_trend_type, figsize=self.study_visualizer.figsize)
axes = self.load_axes_for_trend_test_repartition(self.nb_rows)
else:
assert len(axes) == self.nb_rows
for i, axes_row in enumerate(axes):
trend_type_to_serie = {k: v[i].replace(0.0, np.nan) for k, v in
self.trend_type_to_series(reduction_function).items()}
self.trend_type_to_series(reduction_function, isin_parameters).items()}
vmax = max([s.max() for s in trend_type_to_serie.values()])
vmin = min([s.min() for s in trend_type_to_serie.values()])
vmax = max(vmax, 0.01)
......@@ -162,6 +177,11 @@ class AltitudeHypercubeVisualizer(AbstractHypercubeVisualizer):
plt.suptitle(title)
self.show_or_save_to_file(specific_title=title)
def load_axes_for_trend_test_repartition(self, nb_rows):
nb_trend_type = len(self.display_trend_type_to_style)
fig, axes = plt.subplots(nb_rows, nb_trend_type, figsize=self.study_visualizer.figsize)
return axes
@property
def altitude_index_level(self):
return 0
......@@ -189,18 +209,23 @@ class AltitudeHypercubeVisualizer(AbstractHypercubeVisualizer):
# Take the mean with respect to the level of interest
return df.mean(level=level)
def visualize_altitude_trend_test(self, axes=None, marker='o', add_detailed_plots=False):
def visualize_altitude_trend_test(self, axes=None, marker='o', add_detailed_plots=False, plot_title=None,
isin_parameters=None,
show_or_save_to_file=True):
for subtitle, reduction_function in self.subtitle_to_reduction_function(self.index_reduction,
level=self.altitude_index_level,
add_detailed_plot=add_detailed_plots).items():
self.visualize_trend_test_evolution(reduction_function=reduction_function, xlabel=ALTITUDES_XLABEL,
xlabel_values=self.altitudes, axes=axes, marker=marker,
subtitle=subtitle)
def visualize_massif_trend_test(self, axes=None, add_detailed_plots=False):
last_result = self.visualize_trend_test_evolution(reduction_function=reduction_function,
xlabel=ALTITUDES_XLABEL,
xlabel_values=self.altitudes, axes=axes, marker=marker,
subtitle=subtitle, isin_parameters=isin_parameters,
show_or_save_to_file=show_or_save_to_file,
plot_title=plot_title)
return last_result
def visualize_massif_trend_test(self, axes=None, add_detailed_plots=False, isin_parameters=None):
for subtitle, reduction_function in self.subtitle_to_reduction_function(self.index_reduction,
level=self.massif_index_level,
add_detailed_plot=add_detailed_plots).items():
self.visualize_trend_test_repartition(reduction_function, axes, subtitle=subtitle)
self.visualize_trend_test_repartition(reduction_function, axes, subtitle=subtitle,
isin_parameters=isin_parameters)
from experiment.meteo_france_data.scm_models_data.abstract_extended_study import AbstractExtendedStudy
from experiment.meteo_france_data.scm_models_data.visualization.hypercube_visualization.abstract_hypercube_visualizer import \
AbstractHypercubeVisualizer
from experiment.meteo_france_data.scm_models_data.visualization.hypercube_visualization.altitude_hypercube_visualizer import \
AltitudeHypercubeVisualizer
from experiment.meteo_france_data.scm_models_data.visualization.hypercube_visualization.altitude_year_hypercube_visualizer import \
Altitude_Hypercube_Year_Visualizer
Altitude_Hypercube_Year_Visualizer, AltitudeHypercubeVisualizerBis
class AltitudeHypercubeVisualizerExtended(AltitudeHypercubeVisualizer):
def df_bool(self, display_trend_type):
def df_bool(self, display_trend_type, isin_parameters=None):
df_bool = super().df_bool(display_trend_type)
print(df_bool)
# Slice a part of the array
if isin_parameters is not None:
assert isinstance(isin_parameters, list)
for isin_parameter in isin_parameters:
transpose, values, level = isin_parameter
if transpose:
df_bool = df_bool.transpose()
ind = df_bool.index.isin(values=values, level=level)
res = df_bool.loc[ind].copy()
df_bool = res.transpose() if transpose else res
return df_bool
@property
def region_name_to_isin_parameters(self):
return {region_name: [(False, values, self.massif_index_level)]
for region_name, values in AbstractExtendedStudy.region_name_to_massif_names.items()}
@property
def nb_regions(self):
return len(self.region_name_to_isin_parameters)
def altitude_band_name_to_isin_parameters(self):
return self.altitudes
def visualize_altitute_trend_test_by_regions(self):
return self._visualize_altitude_trend_test(name_to_isin_parameters=self.region_name_to_isin_parameters)
def _visualize_altitude_trend_test(self, name_to_isin_parameters=None):
assert name_to_isin_parameters is not None, 'this method should not be called directly'
multiplication_factor = len(name_to_isin_parameters)
all_axes = self.load_trend_test_evolution_axes(self.nb_rows * multiplication_factor)
specific_title = ''
for j, (name, isin_parameters) in enumerate(name_to_isin_parameters.items()):
axes = all_axes[j::multiplication_factor]
specific_title = self.visualize_altitude_trend_test(axes, plot_title=name, isin_parameters=isin_parameters, show_or_save_to_file=False)
print(specific_title)
self.show_or_save_to_file(specific_title=specific_title)
class AltitudeHypercubeVisualizerBisExtended(AltitudeHypercubeVisualizerExtended, AltitudeHypercubeVisualizerBis):
pass
class AltitudeYearHypercubeVisualizerExtended(AltitudeHypercubeVisualizerExtended, Altitude_Hypercube_Year_Visualizer):
pass
\ No newline at end of file
pass
......@@ -4,16 +4,7 @@ from experiment.meteo_france_data.scm_models_data.visualization.hypercube_visual
AltitudeHypercubeVisualizer
class Altitude_Hypercube_Year_Visualizer(AltitudeHypercubeVisualizer):
def get_title_plot(self, xlabel, ax_idx=None):
if ax_idx == self.nb_axes - 1:
return 'mean starting year'
return super().get_title_plot(xlabel, ax_idx)
@property
def nb_axes(self):
return super().nb_axes + 1
class AltitudeHypercubeVisualizerBis(AltitudeHypercubeVisualizer):
@staticmethod
def index_reduction(df, level, **kwargs):
......@@ -26,10 +17,21 @@ class Altitude_Hypercube_Year_Visualizer(AltitudeHypercubeVisualizer):
# Take the mean with respect to the level of interest
return df.mean(level=level)
def trend_type_reduction_series(self, reduction_function, display_trend_type):
series = super().trend_type_reduction_series(reduction_function, display_trend_type)
class Altitude_Hypercube_Year_Visualizer(AltitudeHypercubeVisualizerBis):
def get_title_plot(self, xlabel, ax_idx=None):
if ax_idx == self.nb_rows - 1:
return 'mean starting year'
return super().get_title_plot(xlabel, ax_idx)
@property
def nb_rows(self):
return super().nb_rows + 1
def trend_type_reduction_series(self, reduction_function, df_bool):
series = super().trend_type_reduction_series(reduction_function, df_bool)
# Create df argmax
df_bool = self.df_bool(display_trend_type)
df = df_bool.copy()
df = (df * df.columns)[df_bool]
# Reduce and append
......
......@@ -5,7 +5,7 @@ from itertools import product
from experiment.meteo_france_data.scm_models_data.visualization.hypercube_visualization.altitude_hypercube_visualizer import \
AltitudeHypercubeVisualizer
from experiment.meteo_france_data.scm_models_data.visualization.hypercube_visualization.altitude_hypercube_visualizer_extended import \
AltitudeYearHypercubeVisualizerExtended
AltitudeYearHypercubeVisualizerExtended, AltitudeHypercubeVisualizerExtended, AltitudeHypercubeVisualizerBisExtended
from experiment.meteo_france_data.scm_models_data.visualization.hypercube_visualization.altitude_year_hypercube_visualizer import \
Altitude_Hypercube_Year_Visualizer
from experiment.meteo_france_data.scm_models_data.visualization.hypercube_visualization.quantity_altitude_visualizer import \
......@@ -84,9 +84,10 @@ def fast_altitude_year_hypercube():
for study_class in SCM_STUDIES[:1]:
for last_starting_year in [None, 1989, 1999][:1]:
for trend_test_class in [GevLocationChangePointTest, GevScaleChangePointTest, GevShapeChangePointTest][:1]:
visualizers = [StudyVisualizer(study, temporal_non_stationarity=True, verbose=False, multiprocessing=True)
for study in study_iterator(study_class=study_class, only_first_one=only_first_one,
altitudes=altitudes)]
visualizers = [
StudyVisualizer(study, temporal_non_stationarity=True, verbose=False, multiprocessing=True)
for study in study_iterator(study_class=study_class, only_first_one=only_first_one,
altitudes=altitudes)]
altitude_to_visualizer = OrderedDict(zip(altitudes, visualizers))
visualizer = Altitude_Hypercube_Year_Visualizer(altitude_to_visualizer, save_to_file=save_to_file,
trend_test_class=trend_test_class,
......@@ -97,7 +98,7 @@ def fast_altitude_year_hypercube():
# visualizer.visualize_massif_trend_test()
def fast_altitude_year_hypercube_extendede():
def fast_altitude_year_hypercube_extended():
save_to_file = False
only_first_one = False
nb_data_reduced_for_speed = True
......@@ -105,16 +106,18 @@ def fast_altitude_year_hypercube_extendede():
for study_class in SCM_STUDIES[:1]:
for last_starting_year in [None, 1989, 1999][:1]:
for trend_test_class in [GevLocationChangePointTest, GevScaleChangePointTest, GevShapeChangePointTest][:1]:
visualizers = [StudyVisualizer(study, temporal_non_stationarity=True, verbose=False, multiprocessing=True)
for study in study_iterator(study_class=study_class, only_first_one=only_first_one,
altitudes=altitudes)]
visualizers = [
StudyVisualizer(study, temporal_non_stationarity=True, verbose=False, multiprocessing=True)
for study in study_iterator(study_class=study_class, only_first_one=only_first_one,
altitudes=altitudes)]
altitude_to_visualizer = OrderedDict(zip(altitudes, visualizers))
visualizer = AltitudeYearHypercubeVisualizerExtended(altitude_to_visualizer, save_to_file=save_to_file,
trend_test_class=trend_test_class,
nb_data_reduced_for_speed=nb_data_reduced_for_speed,
last_starting_year=last_starting_year)
visualizer = AltitudeHypercubeVisualizerExtended(altitude_to_visualizer, save_to_file=save_to_file,
trend_test_class=trend_test_class,
nb_data_reduced_for_speed=nb_data_reduced_for_speed,
last_starting_year=last_starting_year)
# visualizer.visualize_year_trend_test()
visualizer.visualize_altitude_trend_test()
visualizer.visualize_altitute_trend_test_by_regions()
# visualizer.visualize_massif_trend_test()
......@@ -126,7 +129,7 @@ def full_altitude_year_hypercube():
for study_class in SCM_STUDIES[:1]:
for trend_test_class in [GevLocationChangePointTest, GevScaleChangePointTest,
GevShapeChangePointTest][:1]:
years = [1967, 1977, 1987, 1997, 2007, None][:-1][::-1]
years = [1967, 1977, 1987, 1997, 2007, None][-2:-1][::-1]
for last_starting_year in years:
visualizers = [
StudyVisualizer(study, temporal_non_stationarity=True, verbose=False, multiprocessing=True)
......@@ -143,6 +146,32 @@ def full_altitude_year_hypercube():
visualizer.visualize_altitude_trend_test()
def full_altitude_year_hypercube_extended():
save_to_file = True
only_first_one = False
nb_data_reduced_for_speed = False
altitudes = ALL_ALTITUDES[3:-6]
for study_class in SCM_STUDIES[:1]:
for trend_test_class in [GevLocationChangePointTest, GevScaleChangePointTest,
GevShapeChangePointTest][:1]:
years = [1967, 1977, 1987, 1997, 2007, None][-2:-1][::-1]
for last_starting_year in years:
visualizers = [
StudyVisualizer(study, temporal_non_stationarity=True, verbose=False, multiprocessing=True)
for study in study_iterator(study_class=study_class, only_first_one=only_first_one,
altitudes=altitudes)]
altitude_to_visualizer = OrderedDict(zip(altitudes, visualizers))
visualizer = AltitudeHypercubeVisualizerBisExtended(altitude_to_visualizer,
save_to_file=save_to_file,
trend_test_class=trend_test_class,
nb_data_reduced_for_speed=nb_data_reduced_for_speed,
last_starting_year=last_starting_year)
visualizer.visualize_altitute_trend_test_by_regions()
# visualizer.visualize_year_trend_test()
# visualizer.visualize_massif_trend_test()
visualizer.visualize_altitude_trend_test()
def fast_quantity_altitude_hypercube():
save_to_file = False
only_first_one = False
......@@ -166,8 +195,10 @@ def fast_quantity_altitude_hypercube():
def main_run():
# fast_altitude_hypercube()
# fast_altitude_year_hypercube()
fast_altitude_year_hypercube_extendede()
# full_altitude_year_hypercube()
# fast_altitude_year_hypercube_extended()
# full_altitude_year_hypercube_extended()
full_altitude_year_hypercube()
# fast_quantity_altitude_hypercube()
# full_quantity_altitude_hypercube()
......
......@@ -58,8 +58,8 @@ class AbstractUnivariateTest(object):
d = OrderedDict()
# d[cls.POSITIVE_TREND] = 'g--'
# d[cls.NEGATIVE_TREND] = 'r--'
d[cls.ALL_TREND] = 'k--'
d[cls.NON_SIGNIFICATIVE_TREND] = 'b--'
d[cls.ALL_TREND] = 'k-'
d[cls.NON_SIGNIFICATIVE_TREND] = 'b-'
# d[cls.SIGNIFICATIVE_ALL_TREND] = 'k-'
d[cls.SIGNIFICATIVE_POSITIVE_TREND] = 'g-'
d[cls.SIGNIFICATIVE_NEGATIVE_TREND] = 'r-'
......
import unittest
from collections import OrderedDict
import numpy as np
from experiment.meteo_france_data.scm_models_data.safran.safran import SafranSnowfall
from experiment.meteo_france_data.scm_models_data.visualization.hypercube_visualization.altitude_hypercube_visualizer import \
AltitudeHypercubeVisualizer
from experiment.meteo_france_data.scm_models_data.visualization.hypercube_visualization.altitude_year_hypercube_visualizer import \
Altitude_Hypercube_Year_Visualizer
from experiment.meteo_france_data.scm_models_data.visualization.study_visualization.main_study_visualizer import \
study_iterator
from experiment.meteo_france_data.scm_models_data.visualization.study_visualization.study_visualizer import \
StudyVisualizer
from experiment.trend_analysis.univariate_test.abstract_gev_change_point_test import GevLocationChangePointTest
from extreme_estimator.extreme_models.utils import set_seed_for_test
class TestHypercube(unittest.TestCase):
DISPLAY = True
def setUp(self) -> None:
set_seed_for_test(42)
altitudes = [900, 3000]
visualizers = [StudyVisualizer(study, temporal_non_stationarity=True, verbose=False, multiprocessing=True)
for study in study_iterator(study_class=SafranSnowfall, only_first_one=False,
altitudes=altitudes, verbose=self.DISPLAY)]
self.altitude_to_visualizer = OrderedDict(zip(altitudes, visualizers))
self.trend_test_class = GevLocationChangePointTest
self.nb_data_reduced_for_speed = 2
# def test_altitude_hypercube_visualizer(self):
# visualizer = AltitudeHypercubeVisualizer(self.altitude_to_visualizer, save_to_file=False,
# trend_test_class=self.trend_test_class,
# nb_data_reduced_for_speed=self.nb_data_reduced_for_speed,
# verbose=self.DISPLAY)
# self.df = visualizer.df_hypercube_trend_type
def test_year_altitude_hypercube_visualizer(self):
visualizer = Altitude_Hypercube_Year_Visualizer(self.altitude_to_visualizer, save_to_file=False,
trend_test_class=self.trend_test_class,
nb_data_reduced_for_speed=self.nb_data_reduced_for_speed,
verbose=self.DISPLAY)
self.df = visualizer.df_hypercube_trend_type
def tearDown(self) -> None:
if self.DISPLAY:
print(self.df)
# Check that all the rows contain
nb_non_nan_values_per_row = (~self.df.isnull()).sum(axis=1)
equality = nb_non_nan_values_per_row.values == np.ones(len(self.df))
self.assertTrue(equality.all())
if __name__ == '__main__':
unittest.main()
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment