Current section
Files
Jump to
Current section
Files
src/lorenz_test.erl
% Copyright (C) 2014-2019 Olivier Boudeville
%
% This file is part of the Ceylan-Myriad library.
%
% This library is free software: you can redistribute it and/or modify
% it under the terms of the GNU Lesser General Public License or
% the GNU General Public License, as they are published by the Free Software
% Foundation, either version 3 of these Licenses, or (at your option)
% any later version.
% You can also redistribute it and/or modify it under the terms of the
% Mozilla Public License, version 1.1 or later.
%
% This library is distributed in the hope that it will be useful,
% but WITHOUT ANY WARRANTY; without even the implied warranty of
% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
% GNU Lesser General Public License and the GNU General Public License
% for more details.
%
% You should have received a copy of the GNU Lesser General Public
% License, of the GNU General Public License and of the Mozilla Public License
% along with this library.
% If not, see <http://www.gnu.org/licenses/> and
% <http://www.mozilla.org/MPL/>.
%
% Author: Olivier Boudeville [olivier (dot) boudeville (at) esperide (dot) com]
% Unit test mostly for the canvas facility, based on the Lorenz equations to
% show its strange attractor.
%
-module(lorenz_test).
% For run/0 export and al:
-include("test_facilities.hrl").
% For gui-related defines:
-include("gui.hrl").
-export([ get_name/1, draw_lines/3 ]).
% Rendering section.
-type zoom_factor() :: float().
% Description of a simple, local, screen coordinate system:
%
-record( screen, {
center :: linear_2D:point(),
zoom_x :: zoom_factor(),
zoom_y :: zoom_factor()
}).
-type screen() :: #screen{}.
% Resolves the specified equations based on the specified initial conditions and
% derivate function, notifying the specified listener of the new computations.
%
-spec solver_main_loop( rk4_solver:f(), rk4_solver:vector(), rk4_solver:time(),
rk4_solver:time(), screen(), pid() ) -> no_return().
solver_main_loop( F, CurrentPoint, CurrentTime, Timestep, Screen,
ListenerPid ) ->
receive
{ set_time_step, NewTimestep } ->
% Whatever the units may be:
io:format( "Changing time step from ~p to ~p.~n",
[ Timestep, NewTimestep ] ),
solver_main_loop( F, CurrentPoint, CurrentTime, NewTimestep, Screen,
ListenerPid );
stop ->
ok
after 0 ->
% Basic version, one message per point, of course a lot too verbose:
% New point is yn+1, current point is yn, timestep is h:
%NewPoint = compute_next_estimate( F, CurrentPoint, CurrentTime,
% Timestep ),
%io:format( "- new point computed: ~p~n", [ NewPoint ] ),
%ListenerPid ! { draw_point, NewPoint, self() },
% New version: sending a list of PointCount points at once, moreover
% having already projected them on screen coordinates:
PointCount = 50,
{ NewProjectedPoints, LastPoint, NewTime } = compute_next_estimates(
F, CurrentPoint, CurrentTime, Timestep, Screen, PointCount ),
%io:format( "Computed following points: ~w.~n",
% [ NewProjectedPoints ] ),
ListenerPid ! { draw_points, NewProjectedPoints, self() },
% Explicit yielding, otherwise you may experience problems:
timer:sleep( 1 ),
solver_main_loop( F, LastPoint, NewTime, Timestep, Screen,
ListenerPid )
end.
% Returns a list of the next PointCount projected points, the last point
% computed and the corresponding next current time.
%
compute_next_estimates( F, Point, Time, Timestep, Screen, PointCount ) ->
% Clearer than a fold:
compute_next_estimates( F, Point, Time, Timestep, Screen, PointCount,
_Acc=[] ).
compute_next_estimates( _F, Point, NextTime, _Timestep, _Screen, _PointCount=0,
Acc ) ->
{ lists:reverse( Acc ), Point, NextTime } ;
compute_next_estimates( F, Point, Time, Timestep, Screen, PointCount, Acc ) ->
NewPoint = rk4_solver:compute_next_estimate( F, Point, Time, Timestep ),
NewProjectedPoint = project_2D( NewPoint, Screen ),
compute_next_estimates( F, NewPoint, Time + Timestep, Timestep, Screen,
PointCount - 1, [ NewProjectedPoint | Acc ] ).
% Function f( t, v ) corresponding to the equations of the Lorenz system.
%
% See http://en.wikipedia.org/wiki/Lorenz_system
%
lorenz_function( _Time, _Vector={ X0, Y0, Z0 } ) ->
% These equations do not depend on time.
Sigma = 10.0,
Rho = 28.0,
Beta = 8.0 / 3.0,
X1 = Sigma * ( Y0 - X0 ),
Y1 = X0 * ( Rho - Z0 ) - Y0,
Z1 = X0 * Y0 - Beta * Z0,
{ X1, Y1, Z1 }.
% GUI section.
% State of the program, passed between event handlers.
%
-record( gui_state,
{
main_frame,
start_button,
stop_button,
quit_button,
canvas,
screen :: screen(),
% The solver table is an associative table whose keys are the PID of
% each solver, and whose values are { Color, LastPoint } pairs:
%
solver_table :: table:table()
}
).
% The left part of the windows gathers the buttons, while the right one shows
% the canvas.
-spec get_main_window_width() -> linear:coordinate().
get_main_window_width() ->
1920.
-spec get_main_window_height() -> linear:coordinate().
get_main_window_height() ->
1080.
%-spec get_canvas_width() -> linear:coordinate().
%get_canvas_width() ->
% 640.
%-spec get_canvas_height() -> linear:coordinate().
%get_canvas_height() ->
% 480.
% Lists all the declared names of widget identifiers.
%
get_all_id_names() ->
[ 'MainFrame', 'StartButton', 'StopButton', 'QuitButton' ].
% Returns the numerical ID corresponding to the specified name.
%
% (a good target for a parse transform)
%
-spec get_id( atom() ) -> gui:id().
get_id( Name ) ->
list_utils:get_index_of( Name, get_all_id_names() ).
% Returns the name (as an atom) of the specified widget (expected to be named).
%
-spec get_name( gui:id() ) -> atom().
get_name( Id ) ->
Names = get_all_id_names(),
Len = length( Names ),
case Id of
Id when Id < 1 orelse Id > Len ->
unknown;
_ ->
lists:nth( Id, Names )
end.
% Initialises the GUI and associated parts (solver).
%
-spec start() -> no_return().
start() ->
gui:start(),
observer:start(),
FrameSize = { get_main_window_width(), get_main_window_height() },
MainFrame = gui:create_frame( _Title="Lorenz Test", _FramePos=auto,
FrameSize, _FrameStyle=default, _Id=get_id( 'MainFrame' ),
_Parent=undefined ),
gui:connect( MainFrame, close_window ),
StatusBar = gui:create_status_bar( MainFrame ),
SolverCount = system_utils:get_core_count(),
%SolverCount = 2,
%SolverCount = 0,
gui:push_status_text( io_lib:format( "Initialisation of ~B solvers.",
[ SolverCount ] ), StatusBar ),
LeftPanel = gui:create_panel( MainFrame ),
RightPanel = gui:create_panel( MainFrame ),
%gui:set_background_color( MainFrame, red ),
%gui:set_background_color( LeftPanel, blue ),
%gui:set_background_color( RightPanel, green ),
MainSizer = gui:create_sizer( horizontal ),
% Constant width:
gui:add_to_sizer( MainSizer, LeftPanel,
[ { proportion, 0 }, { flag, [ expand_fully ] } ] ),
% Grows with the window:
gui:add_to_sizer( MainSizer, RightPanel,
[ { proportion, 2 }, { flag, [ expand_fully ] } ] ),
ControlBoxSizer = gui:create_sizer_with_labelled_box( vertical, LeftPanel,
"Controls" ),
% Adding the buttons to the control panel:
% Common settings:
Position = auto,
ButtonSize = auto,
ButtonStyle = default,
ParentButton = LeftPanel,
StartButton = gui:create_button( "Start resolution", Position, ButtonSize,
ButtonStyle, get_id( 'StartButton' ), ParentButton ),
gui:connect( StartButton, command_button_clicked ),
StopButton = gui:create_button( "Stop resolution", Position, ButtonSize,
ButtonStyle, get_id( 'StopButton' ), ParentButton ),
gui:connect( StopButton, command_button_clicked ),
QuitButton = gui:create_button( "Quit", Position, ButtonSize, ButtonStyle,
get_id( 'QuitButton' ), ParentButton ),
gui:connect( QuitButton, command_button_clicked ),
gui:set_tooltip( LeftPanel, "Controls for the Lorenz test" ),
ButtonOpt = [ { flag, [ expand_fully ] } ],
gui:add_to_sizer( ControlBoxSizer, StartButton, ButtonOpt ),
gui:add_to_sizer( ControlBoxSizer, StopButton, ButtonOpt ),
gui:add_to_sizer( ControlBoxSizer, QuitButton, ButtonOpt ),
gui:set_sizer( LeftPanel, ControlBoxSizer ),
PolyBoxSizer = gui:create_sizer_with_labelled_box( vertical, RightPanel,
"Phase Space" ),
Canvas = gui_canvas:create( RightPanel ),
gui_canvas:set_background_color( Canvas, red ),
gui_canvas:clear( Canvas ),
gui:connect( Canvas, paint ),
gui:connect( Canvas, size ),
gui:add_to_sizer( PolyBoxSizer, Canvas,
[ { proportion, 1 }, { flag, [ expand_fully ] } ] ),
gui:set_tooltip( Canvas, "Lorenz Attractor." ),
gui:set_sizer( RightPanel, PolyBoxSizer ),
gui:set_sizer( MainFrame, MainSizer ),
% Sets the GUI to visible:
gui:show( MainFrame ),
Screen = #screen{
center={ get_main_window_width() / 3 - 550,
get_main_window_height() / 2 },
zoom_x=24.0,
zoom_y=24.0 },
Colors = gui_color:get_random_colors( SolverCount ),
% The function corresponding to the equation system to solve:
Derivative = fun lorenz_function/2,
% Initial conditions:
InitialPoint = { 0.1, 0.0, 0.0 },
InitialTime = 0.0,
InitialTimestep = 0.005,
SolverTable = create_solver_table( Derivative, Colors, InitialPoint,
InitialTime, InitialTimestep, Screen ),
InitialState = #gui_state{ main_frame=MainFrame,
start_button=StartButton,
stop_button=StopButton,
quit_button=QuitButton,
canvas=Canvas,
screen=Screen,
solver_table=SolverTable
},
erlang:process_flag( priority, _Level=high ),
gui_main_loop( InitialState ),
gui:stop().
% This table helps the rendering process keeping track of the solvers feeding it
% with new points to plot.
%
create_solver_table( Derivative, Colors, InitialPoint, InitialTime,
InitialTimestep, Screen ) ->
create_solver_table( Derivative, Colors, InitialPoint, InitialTime,
InitialTimestep, Screen, _Acc=[] ).
create_solver_table( _Derivative, _Colors=[], _InitialPoint, _InitialTime,
_InitialTimestep, _Screen, Acc ) ->
table:new( Acc );
create_solver_table( Derivative, _Colors=[ C | T ],
_PreviousInitialPoint={ X, Y, Z }, InitialTime,
InitialTimestep, Screen, Acc ) ->
NewInitialPoint = { X + 5.0, Y + 5.0, Z + 5.0 },
% For the closure:
TestPid = self(),
NewSolver = spawn_link( fun() -> solver_main_loop( Derivative,
NewInitialPoint, InitialTime, InitialTimestep, Screen,
TestPid ) end ),
GUIInitialPoint = project_2D( NewInitialPoint, Screen ),
NewAcc = [ { NewSolver, { C, GUIInitialPoint } } | Acc ],
create_solver_table( Derivative, T, NewInitialPoint, InitialTime,
InitialTimestep, Screen, NewAcc ).
%-spec gui_main_loop( gs_object(), integer(), maybe( gui_canvas:canvas() ) )
% -> no_return().
gui_main_loop( _State=#gui_state{ main_frame=_MainFrame,
start_button=_StartButton,
stop_button=_StopButton,
quit_button=_QuitButton,
canvas=_Canvas,
screen=_Screen,
solver_table=_SolverTable } ) ->
%gui_canvas:draw_line( Canvas, { 1, 1 }, { 40, 30 }, red ),
%test_facilities:display( "~nEntering main loop." ),
%% Update = receive
%% % Routine messages sent by solvers shall be listed last, otherwise they
%% % will eclipse other messages (ex: GUI ones):
%% #wx{ obj=MainFrame, event={ wxClose, close_window } } ->
%% test_facilities:display( "Quitting Lorenz test." ),
%% quit;
%% #wx{ obj=StartButton,
%% event=#wxCommand{ type=command_button_clicked } } ->
%% test_facilities:display( "Start button clicked." ),
%% %NewCanvas = render_test( Canvas ),
%% gui_canvas:clear( Canvas ),
%% gui_canvas:draw_line( Canvas, { 1, 40 }, { 40, 1 }, blue ),
%% State#gui_state{ canvas=Canvas };
%% #wx{ obj=StopButton,
%% event=#wxCommand{ type=command_button_clicked } } ->
%% test_facilities:display( "Stop button clicked." ),
%% State#gui_state{ canvas=Canvas };
%% #wx{ obj=QuitButton,
%% event=#wxCommand{ type=command_button_clicked } } ->
%% test_facilities:display( "Quit button clicked." ),
%% [ SolverPid ! stop || SolverPid <- table:keys(
%% State#gui_state.solver_table ) ],
%% quit;
%% #wx{ obj=Any, event=#wxCommand{ type=command_button_clicked } } ->
%% test_facilities:display( "Following button clicked: ~w.", [ Any ] ),
%% quit;
%% % Received for example when another window overlapped:
%% #wx{ event=#wxPaint{} } ->
%% test_facilities:display( "Repainting." ),
%% gui_canvas:clear( Canvas ),
%% gui_canvas:blit( Canvas ),
%% State ;
%% #wx{ event=#wxSize{ size=NewSize } } ->
%% test_facilities:display( "Resizing to ~w.", [ NewSize ] ),
%% NewCanvas = gui_canvas:resize( Canvas, NewSize ),
%% %gui_canvas:clear( NewCanvas ),
%% State#gui_state{ canvas=NewCanvas };
%% { draw_points, NewPoints, SendingSolverPid } ->
%% %io:format( "Drawing ~B points from ~w.~n", [ length( NewPoints ),
%% % SendingSolverPid ] ),
%% { Color, LastPoint } = table:get_value( SendingSolverPid,
%% SolverTable ),
%% NewLastPoint = draw_lines( Canvas, [ LastPoint | NewPoints ],
%% Color ),
%% gui_canvas:blit( Canvas ),
%% NewSolverTable = table:add_entry( _K=SendingSolverPid,
%% _V={ Color, NewLastPoint }, SolverTable ),
%% State#gui_state{ solver_table=NewSolverTable };
%% { draw_point, NewPoint, SendingSolverPid } ->
%% io:format( " - drawing ~p (from ~p)~n",
%% [ NewPoint, SendingSolverPid ] ),
%% { Color, LastPoint } = table:get_value( SendingSolverPid,
%% SolverTable ),
%% SourceDrawPoint = project_2D( LastPoint, Screen ),
%% DestinationDrawPoint = project_2D( NewPoint, Screen ),
%% gui_canvas:draw_line( Canvas, SourceDrawPoint, DestinationDrawPoint,
%% Color ),
%% gui_canvas:blit( Canvas ),
%% NewSolverTable = table:add_entry( _K=SendingSolverPid,
%% _V={ Color, NewPoint }, SolverTable ),
%% State#gui_state{ solver_table=NewSolverTable };
%% Any ->
%% test_facilities:display( "GUI test got event '~w' (ignored).",
%% [ Any ] ),
%% State
%% end,
Update = fixme,
case Update of
quit ->
% Simply stop recursing:
ok;
NewState ->
gui_main_loop( NewState )
end.
% Projects the specified 3D point onto 2D screen system.
%
-spec project_2D( linear_3D:point(), screen() ) -> linear_2D:point().
project_2D( _Point={ X, Y, Z }, #screen{ center={ Xc, Yc },
zoom_x=ZoomX,
zoom_y=ZoomY } ) ->
F = 1 / math:sqrt( 2 ),
{ round( Xc + ZoomX * ( Z - F*X ) ), round( Yc + ZoomY * ( Y - F*X ) ) }.
% Draws lines between all specified (already projected) points, and returns the
% last of these points.
%
draw_lines( _Canvas, _Points=[ LastPoint ], _Color ) ->
LastPoint;
draw_lines( Canvas, _Points=[ P1, P2 | T ], Color ) ->
gui_canvas:draw_line( Canvas, P1, P2, Color ),
draw_lines( Canvas, [ P2 | T ], Color ).
% Runs the test.
%
-spec run() -> no_return().
run() ->
test_facilities:start( ?MODULE ),
case executable_utils:is_batch() of
true ->
test_facilities:display( "(not running the GUI test, "
"being in batch mode)" );
false ->
start()
end,
test_facilities:stop().