diff --git a/README.rst b/README.rst index 530b8c2..531d61c 100644 --- a/README.rst +++ b/README.rst @@ -36,30 +36,112 @@ Project requires: - Pytest (>= 5.4.3) - Pre-commit (>= 2.6.0) +Chaotic model (examples) +~~~~~~~~~~~~~~~~~~~~~~~~ + +Lorenz attractor:: + + dx/dt = sigma * (y - x) + dy/dt = rho * (x - z) - y + dz/dt = x * y - beta * z + +where sigma = 10, rho = 28 and beta = 8/3. + +.. image:: https://raw.githubusercontent.com/capitanov/chaospy/master/img/Lorenz_3d.gif?sanitize=true + +Rossler attractor:: + + dx/dt = sigma * (y - x) + dy/dt = rho * (x - z) - y + dz/dt = x * y - beta * z + +where a = 0.2, b = 0.2 and c = 5.7. + +.. image:: https://raw.githubusercontent.com/capitanov/chaospy/master/img/Rossler_3D.png?sanitize=true + Source code ~~~~~~~~~~~ You can check the latest sources with the command:: - git clone https://github.com/capitanov/chaospy.git + $ git clone https://github.com/capitanov/chaospy.git + $ cd chaospy + $ pip install -r requirements.txt -Chaotic model -~~~~~~~~~~~~~~ +Help +~~~~ + +:: + + usage: parser.py [-h] [-p POINTS] [-s STEP] + [--init_point INIT_POINT [INIT_POINT ...]] [--show_plots] + [--save_plots] [--add_2d_gif] + {lorenz,rossler,rikitake,chua,duffing,wang,nose-hoover,lotka-volterra} + ... + + Specify command line arguments for dynamic system.Calculate some math + parameters and plot some graphs of a given chaotic system. + + optional arguments: + -h, --help show this help message and exit + -p POINTS, --points POINTS + Number of points for dymanic system. Default: 1024. + -s STEP, --step STEP Step size for calculating the next coordinates of + chaotic system. Default: 100. + --init_point INIT_POINT [INIT_POINT ...] + Initial point as string of three floats: "X, Y, Z". + --show_plots Show plots of a model. Default: False. + --save_plots Save plots to PNG files. Default: False. + --add_2d_gif Add 2D coordinates to 3D model into GIF. Default: + False. + + Chaotic models: + You can select one of the chaotic models: + + {lorenz,rossler,rikitake,chua,duffing,wang,nose-hoover,lotka-volterra} + lorenz Lorenz chaotic model + rossler Rossler chaotic model + rikitake Rikitake chaotic model + chua Chua chaotic model + duffing Duffing chaotic model + wang Wang chaotic model + nose-hoover Nose-hoover chaotic model + lotka-volterra Lotka-volterra chaotic model + +Chaotic attractors are used as subparse command. Example: Lorenz attractor +**************** +:: + + usage: parser.py lorenz [-h] [--sigma SIGMA] [--beta BETA] [--rho RHO] + optional arguments: + -h, --help show this help message and exit + + Lorenz model arguments: + --sigma SIGMA Lorenz system parameter. Default: 10 + --beta BETA Lorenz system parameter. Default: 2.6666666666666665 + --rho RHO Lorenz system parameter. Default: 28 + +Chua circuit +************ :: - dx/dt = sigma * (y - x) - dy/dt = rho * (x - z) - y - dz/dt = x * y - beta * z + usage: parser.py chua [-h] [--alpha ALPHA] [--beta BETA] [--mu0 MU0] + [--mu1 MU1] -where sigma= 10, rho= 28 and beta= 8/3. + optional arguments: + -h, --help show this help message and exit -.. image:: https://raw.githubusercontent.com/capitanov/chaospy/master/img/Lorenz_3d.gif?sanitize=true + Chua model arguments: + --alpha ALPHA Chua system parameter. Default: 0.1 + --beta BETA Chua system parameter. Default: 28 + --mu0 MU0 Chua system parameter. Default: -1.143 + --mu1 MU1 Chua system parameter. Default: -0.714 See Also ~~~~~~~~ - `Wikipedia -> chaotic attractors. `__ -- `My articles on habrahabr. (russian language) `__ +- `My articles on habrahabr. (rus lang.) `__ diff --git a/src/dynamic_system.py b/src/dynamic_system.py index 8de284d..2e52513 100644 --- a/src/dynamic_system.py +++ b/src/dynamic_system.py @@ -87,16 +87,16 @@ def __init__(self, input_args: Optional[tuple] = None, show_log: bool = False): # Initialize attributes self.initialize(input_args, show_log) - def initialize(self, input_args: Optional[tuple] = None, show_log: bool = False): + def initialize(self, input_args: Optional[tuple] = None, show_logs: bool = False): # Update parameters - self.settings = Settings(show_log=show_log) - self.settings.update_params(input_args, show_log) + self.settings = Settings(show_logs=show_logs) + self.settings.update_params(input_args) # Update chaotic model self.model = self.settings.model # Update drawer for plots - self.drawer = PlotDrawer(save_plots=self.settings.save_plots, show_plots=self.settings.show_plots) + self.drawer = PlotDrawer(self.settings.save_plots, self.settings.show_plots, self.settings.add_2d_gif) self.drawer.model_name = self.settings.attractor.capitalize() # Update main calculator @@ -118,7 +118,7 @@ def run(self): # Calculate stats = self.collect_statistics(_points) - if self.settings.show_log: + if self.settings.show_logs: print(f"[INFO]: Show statistics:\n{stats}\n") self.calculator.check_probability(_points) @@ -130,18 +130,22 @@ def run(self): # self.drawer.show_time_plots() # self.drawer.show_3d_plots() - self.drawer.make_3d_plot_gif(5) + self.drawer.make_3d_plot_gif(50) # self.drawer.show_all_plots() if __name__ == "__main__": command_line = ( + "--init_point", + "1 -1 2", "--points", - "500", + "2000", "--step", - "100", + "50", "--save_plots", - "lorenz", + # "--show_plots", + "--add_2d_gif", + "rossler", ) dynamic_system = DynamicSystem(input_args=command_line, show_log=True) diff --git a/src/utils/drawer.py b/src/utils/drawer.py index 90fd45f..1a84532 100644 --- a/src/utils/drawer.py +++ b/src/utils/drawer.py @@ -56,12 +56,23 @@ class PlotDrawer: """ - def __init__(self, save_plots: bool = False, show_plots: bool = False, model_name: str = None): + _plot_axis = ((1, 0), (1, 2), (2, 0)) + _plot_labels = {0: "X", 1: "Y", 2: "Z"} + + def __init__( + self, save_plots: bool = False, show_plots: bool = False, add_2d_gif: bool = False, model_name: str = None, + ): self.save_plots = save_plots self.show_plots = show_plots + self.add_2d_gif = add_2d_gif + self.time_or_dot = False + self._model_name = model_name self._coordinates = None + # Internal parameters self._color_map = None + self._plot_list = None + self._min_max_axis = None def __len__(self): return len(self.coordinates) @@ -83,7 +94,7 @@ def coordinates(self, value: np.ndarray): self._coordinates = value @property - @lru_cache(maxsize=8) + @lru_cache(maxsize=16) def min_max_axis(self): return np.vstack([np.min(self.coordinates, axis=0), np.max(self.coordinates, axis=0)]).T @@ -104,17 +115,25 @@ def show_time_plots(self): if self.show_plots: plt.show() - def axis_defaults(self, ax): - ax.set_title(f"{self.model_name} attractor") - ax.set_xlabel("X") - ax.set_ylabel("Y") - ax.set_zlabel("Z") - ax.set_xlim3d(self.min_max_axis[0]) - ax.set_ylim3d(self.min_max_axis[1]) - ax.set_zlim3d(self.min_max_axis[2]) - ax.xaxis.pane.fill = False - ax.yaxis.pane.fill = False - ax.zaxis.pane.fill = False + def _axis_defaults_3d(self, plots): + plots.set_title(f"{self.model_name} attractor") + plots.set_xlabel("X") + plots.set_ylabel("Y") + plots.set_zlabel("Z") + plots.set_xlim3d(self.min_max_axis[0]) + plots.set_ylim3d(self.min_max_axis[1]) + plots.set_zlim3d(self.min_max_axis[2]) + plots.xaxis.pane.fill = False + plots.yaxis.pane.fill = False + plots.zaxis.pane.fill = False + + def _axis_defaults_2d(self, plots): + for idx, (xx, yy) in enumerate(self._plot_axis): + plots[idx].set_xlim(self.min_max_axis[xx]) + plots[idx].set_ylim(self.min_max_axis[yy]) + plots[idx].set_xlabel(self._plot_labels[xx]) + plots[idx].set_ylabel(self._plot_labels[yy]) + plots[idx].grid(True) # ax.set_axis_off() # ax.xaxis.pane.set_edgecolor('w') # ax.yaxis.pane.set_edgecolor('w') @@ -124,23 +143,21 @@ def axis_defaults(self, ax): def show_3d_plots(self): """Plot 3D coordinates as time series.""" - plot_axis = ((1, 0), (1, 2), (2, 0)) - plot_labels = {0: "X", 1: "Y", 2: "Z"} fig = plt.figure(f"3D model of {self.model_name} system", figsize=(8, 6), dpi=100) - for ii, (xx, yy) in enumerate(plot_axis): + for ii, (xx, yy) in enumerate(self._plot_axis): plt.subplot(2, 2, 1 + ii) plt.plot(self.coordinates[:, xx], self.coordinates[:, yy], linewidth=0.75) plt.grid() - plt.xlabel(plot_labels[xx]) - plt.ylabel(plot_labels[yy]) + plt.xlabel(self._plot_labels[xx]) + plt.ylabel(self._plot_labels[yy]) # TODO: 2020/07/26: Set limits! plt.xlim(self.min_max_axis[xx]) plt.ylim(self.min_max_axis[yy]) ax = fig.add_subplot(2, 2, 4, projection="3d") ax.plot(self.coordinates[:, 0], self.coordinates[:, 1], self.coordinates[:, 2], linewidth=0.7) - self.axis_defaults(ax) + self._axis_defaults_3d(ax) plt.tight_layout() if self.save_plots: @@ -148,19 +165,31 @@ def show_3d_plots(self): if self.show_plots: plt.show() + def _add_2d_to_plots(self, figure): + self._plot_list += [figure.add_subplot(2, 2, 1 + ii) for ii in range(3)] + self._axis_defaults_2d(self._plot_list[1:]) + plt.tight_layout() + def make_3d_plot_gif(self, step_size: int = 10): """Make git for 3D coordinates as time series.""" - + nodes = 2 if self.add_2d_gif else 1 + posit = 4 if self.add_2d_gif else 1 fig = plt.figure(f"3D model of {self.model_name} system", figsize=(8, 6), dpi=100) - ax = fig.add_subplot(111, projection="3d") - self.axis_defaults(ax) - (pic,) = plt.plot([], [], ".--", lw=0.75) + + self._plot_list = [fig.add_subplot(nodes, nodes, posit, projection="3d")] + self._axis_defaults_3d(self._plot_list[0]) + + if self.add_2d_gif: + self._add_2d_to_plots(fig) + + # Convert ax to plot + self._plot_list = [item.plot([], [], ".--", lw=0.75)[0] for item in self._plot_list] step_dots = len(self.coordinates) // step_size self._color_map = plt.cm.get_cmap("hsv", step_dots) ani = animation.FuncAnimation( - fig, self.update_coordinates, step_dots, fargs=(step_size, pic), interval=100, blit=False, repeat=True + fig, self.update_coordinates, step_dots, fargs=(step_size,), interval=100, blit=False, repeat=True ) if self.save_plots: @@ -168,10 +197,16 @@ def make_3d_plot_gif(self, step_size: int = 10): if self.show_plots: plt.show() - def update_coordinates(self, num, step, pic): - pic.set_data(self.coordinates[0 : 1 + num * step, 0], self.coordinates[0 : 1 + num * step, 1]) - pic.set_3d_properties(self.coordinates[0 : 1 + num * step, 2]) - pic.set_color(self._color_map(num)) + def update_coordinates(self, num, step): + self._plot_list[0].set_data(self.coordinates[0 : 1 + num * step, 0], self.coordinates[0 : 1 + num * step, 1]) + self._plot_list[0].set_3d_properties(self.coordinates[0 : 1 + num * step, 2]) + self._plot_list[0].set_color(self._color_map(num)) + if self.add_2d_gif: + for ii, (x, y) in enumerate(self._plot_axis): + self._plot_list[ii + 1].set_data( + self.coordinates[0 : 1 + num * step, x], self.coordinates[0 : 1 + num * step, y] + ) + self._plot_list[ii + 1].set_color(self._color_map(num)) def show_all_plots(self): """Cannot show all plots while 'show_plots' is True. @@ -191,12 +226,13 @@ def close_all_plots(): np.random.seed(42) points = np.cumsum(np.random.randn(50, 3), axis=1) - drawer = PlotDrawer(show_plots=True) + drawer = PlotDrawer(show_plots=True, add_2d_gif=True) drawer.coordinates = points drawer.model_name = "Chaotic" # print(drawer.min_max_axis) drawer.make_3d_plot_gif() - # drawer.show_time_plots(coordinates=points) + # drawer.make_3d_plot_gif(show_2d_plots=True) + # drawer.show_time_plots() # drawer.show_3d_plots() # drawer.show_all_plots() diff --git a/src/utils/parser.py b/src/utils/parser.py index 07a2cd3..8121a17 100644 --- a/src/utils/parser.py +++ b/src/utils/parser.py @@ -49,11 +49,10 @@ AttractorType = Union[Chua, Duffing, Rossler, Lorenz, Wang, NoseHoover, Rikitake, Wang, LotkaVolterra] -SET_OF_ATTRACTORS = ("lorenz", "rossler", "rikitake", "duffing", "wang", "nose-hoover", "chua", "lotka-volterra") DEFAULT_PARAMETERS = { "lorenz": {"sigma": 10, "beta": 8 / 3, "rho": 28}, "rikitake": {"a": 1, "mu": 1}, - "duffing": {"a": 0.1, "b": 11}, + "duffing": {"alpha": 0.1, "beta": 11}, "rossler": {"a": 0.2, "b": 0.2, "c": 5.7}, "chua": {"alpha": 0.1, "beta": 28, "mu0": -1.143, "mu1": -0.714}, } @@ -92,159 +91,196 @@ class Settings: "lotka-volterra": LotkaVolterra, } - def __init__(self, show_log: bool = False): - self.show_log = show_log + def __init__(self, show_logs: bool = False, show_help: bool = False): + self.show_logs = show_logs + self.show_help = show_help # Settings self.attractor: str = "lorenz" + self.init_point: Tuple[float, float, float] = (0.1, -0.1, 0.1) self.points: int = 1024 - self.init_point: Tuple[float, float, float] = (0.1, 0.1, 0.1) self.step: float = 10 self.show_plots: bool = False self.save_plots: bool = False + self.add_2d_gif: bool = False self.kwargs: dict = {} # Model self._model: Optional[AttractorType] = None @property - def model(self): + def model(self) -> Optional[AttractorType]: + r"""Return model from dict of attractors. + Set initial parameters. + + """ if self._model is None: self._model = self.__model_map.get(self.attractor)( num_points=self.points, init_point=self.init_point, step=self.step, - show_log=self.show_log, + show_log=self.show_logs, **self.kwargs, ) return self._model - def update_params(self, input_args: Optional[Sequence[str]] = None, show_args: bool = False): + def update_params(self, input_args: Optional[Sequence[str]] = None): r"""Update class attributes from command line parser. Kwargs is a dictionary and it can have some parameters for chaotic model. + Parameters + ---------- + input_args : tuple + Tuple of strings for input arguments. Example: ("--show_plots", "--step", "1000", "lorenz") + """ - args_dict = parse_arguments(input_args=input_args, show_args=show_args) + args_dict = self.parse_arguments(input_args=input_args, show_args=self.show_logs, show_help=self.show_help) for item in args_dict: - if hasattr(self, item): + if hasattr(self, item) and item is not None: setattr(self, item, args_dict[item]) else: self.kwargs[item] = args_dict[item] + self.init_point = args_dict["init_point"] + + @staticmethod + def _three_floats(value) -> Tuple: + values = value.split() + if len(values) != 3: + print(f"[FAIL]: Please enter initial points as X, Y, Z list. Example: --init_point 1 2 3") + raise argparse.ArgumentError + return tuple(map(float, values)) + + def parse_arguments( + self, input_args: Optional[Sequence[str]] = None, show_help: bool = False, show_args: bool = False + ) -> dict: + """This method is an useful command line helper. You can use it with command line arguments. + + Parameters + ---------- + input_args : tuple + + show_help : bool + Show help of argument parser. + + show_args : bool + Display arguments and their values as {key : item} + + Returns + ------- + arguments : dict + Parsed arguments from command line. Note: some arguments are positional. + + Examples + -------- + >>> from src.utils.parser import Settings + >>> settings = Settings() + >>> command_line_str = "lorenz", + >>> test_args = settings.parse_arguments(command_line_str, show_args=True) + [INFO]: Cmmaind line arguments: + points = 1024 + step = 100 + init_point = (0.1, -0.1, 0.1) + show_plots = False + save_plots = False + add_2d_gif = False + attractor = lorenz + sigma = 10 + beta = 2.6666666666666665 + rho = 28 + + >>> command_line_str = "--show_plots rossler --a 2 --b 4".split() + >>> test_args = settings.parse_arguments(command_line_str, show_args=True) + [INFO]: Cmmaind line arguments: + points = 1024 + step = 100 + init_point = (0.1, -0.1, 0.1) + show_plots = True + save_plots = False + add_2d_gif = False + attractor = rossler + a = 2.0 + b = 4.0 + c = 5.7 -def parse_arguments( - input_args: Optional[Sequence[str]] = None, show_help: bool = False, show_args: bool = False -) -> dict: - """This method is an useful command line helper. You can use it with command line arguments. - - Parameters - ---------- - input_args: tuple - - show_help : bool - Show help of argument parser. - - show_args : bool - Display arguments and their values as {key : item} - - Returns - ------- - arguments : dict - Parsed arguments from command line. Note: some arguments are positional. - - Examples - -------- - >>> from src.utils.parser import parse_arguments - >>> command_line_str = "lorenz", - >>> test_args = parse_arguments(command_line_str, show_args=True) - points = 1024 - step = 100 - show_plots = False - save_plots = False - attractor = lorenz - sigma = 10 - beta = 2.6666666666666665 - rho = 28 - >>> command_line_str = "--show_plots rossler --a 2 --b 4".split() - >>> test_args = parse_arguments(command_line_str, show_args=True) - points = 1024 - step = 100 - show_plots = True - save_plots = False - attractor = rossler - a = 2.0 - b = 4.0 - c = 5.7 - >>> command_line_str = "--step 1 --points 10 wang".split() - >>> test_args = parse_arguments(command_line_str) - >>> print(test_args) - Namespace(attractor='wang', points=10, save_plots=False, show_plots=False, step=1) - """ - parser = argparse.ArgumentParser( - description="Specify command line arguments for dynamic system." - "Calculate some math parameters and plot some graphs of a given chaotic system." - ) - - parser.add_argument( - "-p", - "--points", - type=int, - default=1024, - action="store", - help=f"Number of points for dymanic system. Default: 1024", - ) - - parser.add_argument( - "-s", - "--step", - type=int, - default=100, - action="store", - help=f"Step size for calculating the next coordinates of chaotic system. Default: 100", - ) - - parser.add_argument("--show_plots", action="store_true", help="Show plots of a model. Default: False.") - parser.add_argument("--save_plots", action="store_true", help="Save plots to PNG files. Default: False.") - - subparsers = parser.add_subparsers( - title="Chaotic models", description="You can select one of the chaotic models", dest="attractor" - ) - - sub_list = [] - for attractor in SET_OF_ATTRACTORS: - chosen_items = DEFAULT_PARAMETERS.get(attractor) - chosen_model = f"{attractor}".capitalize() - subparser = subparsers.add_parser(f"{attractor}", help=f"{chosen_model} chaotic model") - if chosen_items is not None: - group = subparser.add_argument_group(title=f"{chosen_model} model arguments") - for key in chosen_items: - group.add_argument( - f"--{key}", - type=float, - default=chosen_items[key], - action="store", - help=f"{chosen_model} system parameter. Default: {chosen_items[key]}", - ) - sub_list.append(subparser) - - if show_help: - parser.print_help() - for item in sub_list: - item.print_help() - - args = vars(parser.parse_args(input_args)) - if args["attractor"] is None: - raise AssertionError(f"[FAIL]: Please select a chaotic model from the next set: {SET_OF_ATTRACTORS}") - if show_args: - print(f"[INFO]: Cmmaind line arguments: ") - for arg in args: - print(f"{arg :<14} = {args[arg]}") - print("") - return args + """ + parser = argparse.ArgumentParser( + description="Specify command line arguments for dynamic system." + "Calculate some math parameters and plot some graphs of a given chaotic system." + ) + + parser.add_argument( + "-p", + "--points", + type=int, + default=1024, + action="store", + help=f"Number of points for dymanic system. Default: 1024.", + ) + + parser.add_argument( + "-s", + "--step", + type=int, + default=100, + action="store", + help=f"Step size for calculating the next coordinates of chaotic system. Default: 100.", + ) + + parser.add_argument( + "--init_point", + action="store", + type=self._three_floats, + default=(0.1, -0.1, 0.1), + help='Initial point as string of three floats: "X, Y, Z".', + ) + parser.add_argument("--show_plots", action="store_true", help="Show plots of a model. Default: False.") + parser.add_argument("--save_plots", action="store_true", help="Save plots to PNG files. Default: False.") + parser.add_argument( + "--add_2d_gif", action="store_true", help="Add 2D coordinates to 3D model into GIF. Default: False." + ) + + subparsers = parser.add_subparsers( + title="Chaotic models", description="You can select one of the chaotic models:", dest="attractor" + ) + + sub_list = [] + for attractor in [*self.__model_map]: + chosen_items = DEFAULT_PARAMETERS.get(attractor) + chosen_model = f"{attractor}".capitalize() + subparser = subparsers.add_parser(f"{attractor}", help=f"{chosen_model} chaotic model") + if chosen_items is not None: + group = subparser.add_argument_group(title=f"{chosen_model} model arguments") + for key in chosen_items: + group.add_argument( + f"--{key}", + type=float, + default=chosen_items[key], + action="store", + help=f"{chosen_model} system parameter. Default: {chosen_items[key]}", + ) + sub_list.append(subparser) + + if show_help: + parser.print_help() + for item in sub_list: + item.print_help() + + args = vars(parser.parse_args(input_args)) + if args["attractor"] is None: + raise AssertionError(f"[FAIL]: Please select a chaotic model from the next set: {[*self.__model_map]}") + if show_args: + print(f"[INFO]: Cmmaind line arguments:") + for arg in args: + print(f"{arg :<14} = {args[arg]}") + + if args["init_point"] is not None and len(args["init_point"]) != 3: + raise AssertionError(f"[FAIL]: Please enter initial points as X, Y, Z list. Example: --init_point 1 2 3") + return args if __name__ == "__main__": - # import doctest - # - # doctest.testmod(verbose=True) + import doctest - args_main = parse_arguments(show_args=True) - print(args_main) + doctest.testmod(verbose=True) + # command_line = "--init_point", '0 -1 2', "lorenz" + # params = Settings(show_logs=True, show_help=True) + # params.update_params(input_args=command_line)