Skip to content

Commit 0123520

Browse files
authored
Add floodfill (#1060)
* add floodfill * flake8 * import floodfill * add floodfill * add floodfill * add all * import all from floodfill_acceptance * moving track_queue * explicit kwargs * fix arguments * change default window * fix DocString and default arguments * ruff ok * modify to provide all track data * add floodfill info and flags * modify to use floodfill * add DocString example * fix index * isort and black * isort and black * ruff fix * fix flake8 * fix ruff and flake8 inside floodfill
1 parent 0a91047 commit 0123520

File tree

6 files changed

+491
-14
lines changed

6 files changed

+491
-14
lines changed

atmat/pubtools/Contents.m

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
% calc_TouschekPM - TauT = calc_TouschekPM(TD,dppPM,Trf,Ib,U0,coupling, sigE, emit_x)
1515
% freqsearch - =========================================================================
1616
% nlchromplot - Example: nlchromplot(esrf,-.04,.04,30,16,1)
17+
% atfloodfill - Explores the acceptance from the unstable to the stable region
1718
%
1819
% PUBTOOLS/APERTURE
1920
% SetPhysicalAperture - Ringapert=SetPhysicalAperture(ring,apertureX,apertureY)

atmat/pubtools/atfloodfill.m

Lines changed: 191 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,191 @@
1+
function [pnotlost, plost] = atfloodfill(ring, varargin)
2+
% [pnotlost, plost] = floodfill(ring)
3+
%
4+
% Finds the 2D acceptance of the ring using Flood Fill.
5+
%
6+
% Flood fill tracks particles from the exterior to the border of the
7+
% acceptance.
8+
% The lost particles are returned in plost.
9+
% The not lost particles are returned in pnotlost.
10+
%
11+
% Parameters:
12+
% ring: AT lattice.
13+
%
14+
% Keyword Arguments:
15+
% nturns: Number of turns for the tracking. Default: 1000
16+
% window: Min and max coordinate range
17+
% [Axis1min,Axis1max,Axis2min,Axis2max].
18+
% Default [-10e-3,10e-3,-10e-3,10e-3].
19+
% Axis1 and Axis2 are defined by 'axes'.
20+
% gridsize: Number of steps per axis. Default [10,10].
21+
% axes: Indexes of axes to be scanned. Default is [1,3], i.e. x-y.
22+
% sixdoffset: Offset to be added. Default zeros(6,1).
23+
% This is useful to study off-axis acceptance on any plane,
24+
% or off-momentum acceptance by adding dp to the 5th coord.,
25+
% to track particles on the closed orbit, or to add
26+
% a small deviation to the tracked coordinates,
27+
% e.g. [10e-5 10e-5] in the transverse planes.
28+
% verbose: Print extra info. Default 0.
29+
%
30+
% Returns:
31+
% pnotlost: array of size (2,n_not_lost) containing the 2D offsets of
32+
% n_not_lost tracked particles that survived.
33+
% plost: array of size (3,n_lost) containing the 2D offsets of
34+
% n_lost trackedp articles that did not survive; and
35+
% in the third row the turn on which each particle is lost.
36+
%
37+
% Example:
38+
% [pnl, pl] = atfloodfill(THERING, nturns=500)
39+
%
40+
%
41+
% Notes:
42+
% This method is recomended for single or low number of CPUs, and,
43+
% it does not scale well for parallel computing.
44+
%
45+
% Based on the article,
46+
% B. Riemann, M. Aiba, J. Kallestrup, and A. Streun, "Efficient
47+
% algorithms for dynamic aperture and momentum acceptance
48+
% calculation in synchrotron light sources", Phys. Rev. Accel.
49+
% Beams, vol. 27, no. 9, p. 094 002, 2024.
50+
% doi:10.1103/PhysRevAccelBeams.27.094002
51+
52+
% Author : E. Serra, UAB and ALBA, 2025 original version in python
53+
% See. IPAC2025, MOPB065.
54+
% APPLICATION OF FAST ALGORITHMS TO
55+
% CALCULATE DYNAMIC AND
56+
% MOMENTUM APERTURE TO THE DESIGN OF
57+
% ALBA II.
58+
% doi: 10.18429/JACoW-IPAC25-MOPB065
59+
% Edited : O. Blanco, ALBA, 2025 matlab version
60+
61+
% %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
62+
% Parse optional arguments
63+
p = inputParser;
64+
addOptional(p,'nturns',1000);
65+
addOptional(p,'window',[-10e-3,10e-3,-10e-3,10e-3]);
66+
addOptional(p,'gridsize',[10,10]);
67+
addOptional(p,'axes',[1,3]);
68+
addOptional(p,'sixdoffset',zeros(6,1));
69+
addOptional(p,'verbose',0);
70+
parse(p,varargin{:});
71+
par = p.Results;
72+
73+
% Initialize variables
74+
nturns = par.nturns;
75+
window = par.window;
76+
gridsize = par.gridsize;
77+
sixdoffset = par.sixdoffset;
78+
verbose = par.verbose;
79+
axes = par.axes;
80+
81+
% Initialize output in case we return earlier
82+
pnotlost = zeros(2,0);
83+
plost = zeros(3,0);
84+
85+
if verbose, fprintf('Flood fill starts.\n'); end
86+
87+
if verbose, fprintf('Max. number of turns: %d\n',nturns); end
88+
89+
% Create the grid
90+
nx = gridsize(1);
91+
ny = gridsize(2);
92+
npart = nx*ny;
93+
94+
% Check the window dimensions
95+
if (window(1) == window(2) || window(3) == window(4)) && verbose
96+
fprintf('Window is quite narrow.\n');
97+
return;
98+
end
99+
% Check the grid
100+
if any(gridsize < 2)
101+
fprintf('Horizontal and vertical gridsize is too small.\n');
102+
return;
103+
end
104+
xvals = linspace(min(window(1:2)),max(window(1:2)),nx);
105+
yvals = linspace(min(window(3:4)),max(window(3:4)),ny);
106+
points = zeros(2,npart);
107+
108+
if verbose, fprintf('Number of grid points: %d.\n',npart); end
109+
110+
% Set the order
111+
ii = 1;
112+
for ix = 1:nx
113+
for iy = 1:ny
114+
points(:,ii) = [xvals(ix),yvals(iy)]';
115+
ii = ii + 1;
116+
end
117+
end
118+
119+
% Particle coordinates
120+
particles = zeros(6,npart);
121+
particles = particles + sixdoffset;
122+
particles(axes(1),:) = particles(axes(1),:) + points(1,:);
123+
particles(axes(2),:) = particles(axes(2),:) + points(2,:);
124+
125+
% Track the particles for nturns using the flood-fill algorithm.
126+
% the_queue is replaced by an array with a queue_counter in matlab.
127+
% The order is fixed to explore the top, left, bottom and right sides.
128+
the_queue = zeros(npart,1);
129+
the_queue(1 :(ny-2) ) = ((nx-1)*ny+2):(nx*ny-1); %r
130+
the_queue((ny-1) :(nx+ny-2) ) = ((nx-1):-1:0)*ny+1; %b
131+
the_queue((nx+ny-1) :(nx+ny+ny-4) ) = 2:(ny-1); %l
132+
the_queue((nx+ny+ny-3):(2*nx + 2*(ny-2))) = (nx:-1:1)*ny; %t
133+
queue_counter = 2*nx + 2*(ny-2);
134+
135+
% particles that have been tracked
136+
idxpartdone = false(npart,1);
137+
138+
% output
139+
plost = cell(npart,1);
140+
pnotlost = cell(npart,1);
141+
142+
if verbose, fprintf('Tracking... '); end
143+
while queue_counter > 0
144+
% take one index from the_queue and track that particle
145+
i = the_queue(queue_counter);
146+
queue_counter = queue_counter - 1;
147+
% check if valid and not done
148+
if (1 <= i && i <= npart) && ~idxpartdone(i)
149+
idxpartdone(i) = true;
150+
[~, ~, ~, lossinfo] = ringpass(ring, particles(:,i), nturns);
151+
152+
if lossinfo.lost
153+
% if lost, add new points
154+
newidx = [i+1 i-1 i+ny i-ny];
155+
maskidx = newidx > 0 & newidx <= npart;
156+
newidx = newidx(maskidx);
157+
nextpoints = newidx(~idxpartdone(newidx));
158+
queue_counter_aux = queue_counter+length(nextpoints);
159+
the_queue(queue_counter+1:queue_counter_aux) = nextpoints;
160+
queue_counter = queue_counter_aux;
161+
162+
% save point
163+
plost{i} = [ ...
164+
particles(axes(1),i); ...
165+
particles(axes(2),i); ...
166+
lossinfo.turn ...
167+
];
168+
else
169+
% if not lost, save point
170+
pnotlost{i} = [ ...
171+
particles(axes(1),i); ...
172+
particles(axes(2),i); ...
173+
];
174+
end
175+
end
176+
end
177+
178+
if verbose, fprintf(' done.\n'); end
179+
180+
% remove empty cells
181+
plost = plost(~cellfun('isempty',plost));
182+
pnotlost = pnotlost(~cellfun('isempty',pnotlost));
183+
184+
% reshape output
185+
plost = reshape(cell2mat(plost),3,[]);
186+
pnotlost = reshape(cell2mat(pnotlost),2,[]);
187+
188+
if isempty(pnotlost) && verbose
189+
fprintf('No surviving particles.\n');
190+
end
191+
end

pyat/at/acceptance/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,3 +5,4 @@
55
from .acceptance import *
66
from .touschek import *
77
from .momap_alternative import *
8+
from .floodfill_acceptance import *

pyat/at/acceptance/acceptance.py

Lines changed: 23 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -15,12 +15,13 @@
1515

1616
import numpy as np
1717

18-
from .boundary import GridMode
18+
from at.lattice import AtError
19+
20+
from ..lattice import Lattice, Refpts, frequency_control
1921
from ..tracking import MPMode, gpu_core_count
2022

2123
# noinspection PyProtectedMember
22-
from .boundary import boundary_search
23-
from ..lattice import Lattice, Refpts, frequency_control
24+
from .boundary import GridMode, boundary_search
2425

2526

2627
@frequency_control
@@ -41,7 +42,8 @@ def get_acceptance(
4142
divider: int = 2,
4243
shift_zero: float = 1.0e-6,
4344
start_method: str | None = None,
44-
**kwargs
45+
floodfill: bool = False,
46+
**kwargs,
4547
):
4648
# noinspection PyUnresolvedReferences
4749
r"""Computes the acceptance at ``repfts`` observation points
@@ -89,6 +91,9 @@ def get_acceptance(
8991
Windows is ``'spawn'``. ``'fork'`` may be used on macOS to speed up
9092
the calculation or to solve runtime errors, however it is
9193
considered unsafe.
94+
floodfill: scan from unstable to stable. Only in `GridMode.CARTESIAN`,
95+
and CPU tracking. GPU not implemented.
96+
pool_size: number of CPUs. Only has effect with floodfill
9297
9398
Returns:
9499
boundary: (2,n) array: 2D acceptance
@@ -124,6 +129,15 @@ def get_acceptance(
124129
if use_mp is True:
125130
use_mp = MPMode.CPU
126131

132+
if floodfill and (grid_mode is not GridMode.CARTESIAN):
133+
raise AtError("floodfill requires GridMode.CARTESIAN")
134+
if floodfill and (use_mp is MPMode.GPU):
135+
raise AtError("floodfill is not implemented for GPU tracking")
136+
if floodfill:
137+
kwargs["floodfill"] = True
138+
if "pool_size" not in kwargs:
139+
kwargs["pool_size"] = multiprocessing.cpu_count()
140+
127141
if verbose:
128142
nproc = multiprocessing.cpu_count()
129143
print(f"\n{nproc} cpu found for acceptance calculation")
@@ -198,7 +212,7 @@ def get_1d_acceptance(
198212
divider: int | None = 2,
199213
shift_zero: float | None = 1.0e-6,
200214
start_method: str | None = None,
201-
**kwargs
215+
**kwargs,
202216
):
203217
r"""Computes the 1D acceptance at ``refpts`` observation points
204218
@@ -271,9 +285,9 @@ def get_1d_acceptance(
271285
assert np.isscalar(amplitude), "1D acceptance: scalar args required"
272286
npoint = np.ceil(amplitude / resolution)
273287
if grid_mode is not GridMode.RECURSIVE:
274-
assert npoint > 1, (
275-
"Grid has only one point: increase amplitude or reduce resolution"
276-
)
288+
assert (
289+
npoint > 1
290+
), "Grid has only one point: increase amplitude or reduce resolution"
277291
b, s, g = get_acceptance(
278292
ring,
279293
plane,
@@ -290,7 +304,7 @@ def get_1d_acceptance(
290304
divider=divider,
291305
shift_zero=shift_zero,
292306
offset=offset,
293-
**kwargs
307+
**kwargs,
294308
)
295309
return np.squeeze(b), s, g
296310

pyat/at/acceptance/boundary.py

Lines changed: 47 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,18 @@
44
grid definitions
55
"""
66

7-
from at.lattice import Lattice, AtError, AtWarning, Refpts
8-
from typing import Optional, Sequence
7+
import time
8+
import warnings
9+
from collections import namedtuple
910
from enum import Enum
11+
from typing import Optional, Sequence
12+
1013
import numpy as np
1114
from scipy.ndimage import binary_dilation, binary_opening
12-
from collections import namedtuple
13-
import time
14-
import warnings
15+
16+
from at.lattice import AtError, AtWarning, Lattice, Refpts
17+
18+
from .floodfill_acceptance import floodfill
1519

1620
__all__ = ["GridMode"]
1721

@@ -343,6 +347,44 @@ def grid_boundary_search(
343347
if offset is None:
344348
offset = [None for _ in np.arange(len(obspt))]
345349

350+
if kwargs.get("floodfill"):
351+
s_ffallrefpts = []
352+
g_ffallrefpts = []
353+
for obs, orbit in zip(obspt, offset, strict=True):
354+
# set ring and offsets
355+
_obs = 0 if obs is None else obs
356+
_dpp = 0.0 if dp is None else dp
357+
_orbit, _ringrot = set_ring_orbit(ring, _dpp, _obs, orbit)
358+
_offset = _orbit + np.array([0, 0, 0, 0, _dpp, 0])
359+
# flood fill
360+
window = np.ravel((np.array(config.bounds).T * config.amplitudes).T)
361+
data_ff = floodfill(
362+
_ringrot,
363+
nturns=nturns,
364+
window=window,
365+
grid_size=config.shape,
366+
axes=config.planesi,
367+
offset=_offset,
368+
verbose=False,
369+
use_mp=use_mp,
370+
pool_size=kwargs["pool_size"],
371+
)
372+
mask_alive = data_ff[2, :] == 0
373+
s_ffallrefpts.append(data_ff[0:2, mask_alive])
374+
g_ffallrefpts.append(data_ff[0:2, :])
375+
if len(obspt) == 1:
376+
s_ff = s_ffallrefpts[0]
377+
g_ff = g_ffallrefpts[0]
378+
else:
379+
s_ff = s_ffallrefpts
380+
g_ff = g_ffallrefpts
381+
# floodfill boundary and survived are the same,
382+
# but they need to be sorted
383+
_ia = np.argsort(np.arctan2(s_ff[1, :], s_ff[0, :]))
384+
b_ff = s_ff[:, _ia]
385+
# we return boundary, survived, tracked
386+
return b_ff, s_ff, g_ff
387+
346388
for i, obs, orbit in zip(np.arange(len(obspt)), obspt, offset):
347389
orbit, _ = set_ring_orbit(ring, dp, obs, orbit)
348390
parts, grid = get_parts(config, orbit)

0 commit comments

Comments
 (0)