text:
\section{Introduction}
The Versal Adaptive Compute Acceleration Platform (ACAP) is a new type of FPGA which combines Programmable Logic (PL) with other facets including CPU-based Programmable Subsystem (PS) and AI engines \cite{gaide2019xilinx}. These AI Engines, or AIEs and we use these two terms interchangeably throughout this paper, are of specific interest here as they are designed to accelerate highly-parallel vector operations. The Versal AI-series contains up to 400 engines running between 1 and 1.2 GHz, and each engine follows a Very Long Instruction Word (VLIW) design, capable of issuing seven instructions per cycle. AI engines are capable of undertaking 8-way vectorized single-precision floating point operations and up to 128 8 bit fixed point arithmetic operations per cycle.

The large amount of raw compute provided by the AIEs is interesting for High Performance Computing (HPC) workloads, where the ability to use the Versal's PL to tailor memory accesses bespoke to an application and the AI engines to accelerate the compute has potential. To date there have been a very limited number of preliminary AIE studies \cite{lee2021preliminary} \cite{zhang2022h}, and-so an important outstanding question is whether these engines can be effectively leveraged for real world HPC kernels. In this work we use the atmospheric advection kernel of the Met Office NERC Cloud model (MONC) \cite{easc}, which is an open source high resolution atmospheric modelling framework, as a vehicle to explore the AI engines. Following a stencil-based compute pattern, which is very common in HPC codes, in this short paper we explore how to best map this compute pattern onto the AIEs and how performance compares against other approaches. This paper is structured as follows, in Section \ref{sec:bg} we explore the background to this work before summarising the experimental setup in Section \ref{sec:experimental_setup}. Section \ref{sec:aie_focus} explores structuring our AIE kernel(s) and interfacing these with the PL, before undertaking a performance comparison against other hardware in Section \ref{sec:multiple}. We then conclude and discuss recommendations in Section \ref{sec:conclusions}.

The novel contributions of this paper are \textbf{1)} An exploration of techniques to most effectively structure AIE kernels \textbf{2)} An initial performance comparison between the AIEs and other hardware \textbf{3)} Highlighting some of the limitations of the current AIE technology that one must consider when working with the hardware.

\section{Background}
\label{sec:bg}
\subsection{The Versal AI engines}
\label{sec:aie}
The VLIW design of Xilinx's new AI engines is such that, per cycle, each engine is capable of issuing a maximum of two loads, one store, one scalar operation, one fixed point or floating point vector operation, and two move instructions. The vector unit is of size 256 bits, and focusing on single precision floating point arithmetic in this paper, each engine is capable of undertaking up to eight single precision floating point calculations per cycle. Consequently it is important to ensure code is correctly vectorized to obtain best performance on the AIEs. Based on 400 AI engines running at 1.2GHz on the VCK5000, there is a theoretical single precision floating point performance of 3.6 TFLOPS.

AI engines are arranged in a 2D array, with engines connected to their neighbours in both dimensions. Each engine contains 16KB of program memory and 32KB of local data memory and, for the later, is able to directly access the memories of three of its neighbours providing a total of 128KB contiguous addressable data memory \cite{aie_best_practice}. Furthermore, each engine has two 32 bit input streams and two 32 bit output streams which are combined with a FIFO to provide 128 bit access every four clock cycles. Lastly, AI engines connect to one of their neighbours via a cascade stream which is 384 bits wide and designed to allow arithmetic operations to be chained.

AIE code comprises two parts, kernels which will be mapped to AI engines and a graph description which connects kernels together via their streams and memories, as well as to the PL. Programmatically there are two ways in which data can enter or leave a kernel, windows and streams \cite{aie_programming_guide}. Windows provide a buffer, where the current data position in the window is tracked. For input windows data is consumed from this buffer by the kernel, for output windows data is written. The other approach, a stream, provides an infinite number of scalars and vectors that can be read and written by the kernel. There underlies an important difference between these two approaches, where a window of data will only progress to the next window between outer iterations of the kernel, as driven by the AIE graph, whereas streams can continually be read from and written to inside the kernel. Consequently, with windows one must frequently start and stop their kernels to refresh the window data, which is not required with streams.

Whilst the AI engines are the major focus in this paper, it is also important to highlight the general architectural improvements that Xilinx have made to the PL in their Versal series. Built on a 7nm process technology, numerous components including DDR controllers and PCIe interface have been hardened compared to previous generations \cite{ahmad2019xilinx}. Furthermore, a dedicated Network on Chip (NoC) is provided which not only connects the PL with the AIEs, but can also be used between IP blocks on the PL.

\subsection{Piacsek and Williams advection kernel}

Advection is the movement of values through the atmosphere due to wind and, at around 40\% of the runtime, is the single longest running piece of functionality in the MONC model \cite{easc}. The code loops over three fields;  \emph{U}, \emph{V} and \emph{W}, representing wind velocity in the \emph{x}, \emph{y} and \emph{z} dimensions respectively. This Piacsek and Williams (PW) \cite{pwadvection} advection scheme is called each timestep of the model and calculates advection results, otherwise known as \emph{source terms}, for each field. This advection scheme is a stencil based algorithm, of depth one, where calculating the value of a grid cell requires contributions from neighbouring values across all three dimensions.

In previous work \cite{brown2021accelerating} this kernel was ported to an Alveo U280 using High Level Synthesis (HLS) and leveraging the \emph{dataflow} HLS pragma to run multiple components concurrently. The structure of this HLS kernel is illustrated in Figure \ref{fig:df_design}, where the boxes are dataflow regions and arrows between these are internal HLS streams. 3D shift buffers provide a bespoke memory solution which is capable of delivering all 27-stencil values per cycle to the advection compute stages, which was found to be the optimal approach even though not all 27 neighbouring stencil values are required by the advection calculations. Given this existing structure it was our hypothesis that we could replace the advection calculation stages with streams to and from the AI engines, still leveraging the existing tailoring of memory accesses on the PL that worked well in \cite{brown2021accelerating}, with the raw compute power of the AI engines.

\begin{figure}[h]
  \centering
  \includegraphics[scale=0.55]{images/df_design.png}
  \caption{Dataflow design of HLS advection kernel from \cite{brown2021accelerating}}
  \label{fig:df_design}
\end{figure}

\section{Experimental setup}
\label{sec:experimental_setup}
In this work we are using a Xilinx VCK5000 containing a Versal VC1902 ACAP and 16GB of DDR4-DRAM. All VCK5000 runs are built using Vitis 2022.1, the PL is running at 300MHz, and the VC1902 contains 400 AI engines running at 1.2GHz. We compare against an Alveo U280 which contains 8GB of HBM2, is also running at 300MHz, and Alveo kernels are built using Vitis 2021.1. Both the VCK5000 and Alveo U280 are PCIe based cards hosted by a machine containing a 32-core AMD EPYC 7502 processor and 256GB DRAM.

All reported results are averaged over five runs and performance results are reported as useful FLOPS, which is the number of floating point operations undertaken that contribute to the calculation's result. Our performance numbers measure device-side execution time only and do not include the time taken to copy input data to, or result data from, the host and device. This is because we are most interested in the performance of the AIEs in this work, and device-side performance therefore provides a clearer picture when comparing against other technologies that exhibit different host-device data transfer overheads.

\section{AIE porting and optimisation}
\label{sec:aie_focus}
We started by decomposing the advection stencil-based calculation into constituent operations, resulting in, for each grid cell, the code undertaking six additions, followed by six multiplications, then four subtractions and finally an addition reduction to sum these subtractions together. A floating point vector of size six is not supported by the tooling and-so we pad with an additional two empty values to make a vector of size eight. This is why we report useful, rather than total, FLOPS, as useful FLOPS ignores the processing of these empty values by only considering those floating point operations that actually contribute to the advection result.

\begin{figure}[h]
  \centering
  \includegraphics[width=\linewidth]{images/calc.png}
  \caption{Illustration of AIE calculations per grid cell, with the numbers representing the number of single precision floating point numbers provided.}
  \label{fig:calc}
\end{figure}

The structure of this kernel is illustrated in Figure \ref{fig:calc}, with the first 8-way vector addition requiring sixteen floating point numbers comprising the operands. The multiplication requires an additional eight input numbers which are multiplied by the result of the preceding addition. We packaged this as a single AIE kernel and Listing \ref{lst:kernel_code} provides a partial sketch of the code. In order to prepare for the vector addition, streams of four numbers are read and loaded into the appropriate locations of the \emph{lhs} and \emph{rhs} vectors in lines 11 to 14. These vectors are then provided as arguments to the \emph{aie::add} method at line 16, which undertakes the vectorized addition. Multiplication, subtraction, and reductions operations are handled similarly and omitted for brevity. It can be seen at line 6 that we are looping over grid cells, and the directives at lines 7 and 8 instruct the AIE compiler to undertake software pipelining where possible, attempting to keep the VLIW slots filled as per Xilinx's best practice \cite{aie_best_practice}.

\begin{lstlisting}[frame=lines,caption={Sketch of AIE advection kernel code}, label={lst:kernel_code}, numbers=left]
void cell_advection(input_stream<float> * __restrict in_A, input_stream<float> * __restrict in_B, output_stream<float> * __restrict out) {
  aie::vector<float, 4> in_data;
  in_data=readincr_v<4>(in_A);

  int32 cells=(int32) in_data.get(0);
  for (int i=0;i<cells;i++)
  chess_prepare_for_pipelining
  chess_loop_range(64,) {
    aie::vector<float,8> lhs_nums, rhs_nums;

    lhs_nums.insert(0,readincr_v<4>(in_A));
    lhs_nums.insert(1,readincr_v<4>(in_A));
    rhs_nums.insert(0,readincr_v<4>(in_B));
    rhs_nums.insert(1,readincr_v<4>(in_B));

    aie::vector<float,8> vadd=aie::add(lhs_nums,rhs_nums);
    ....
  }
\end{lstlisting}

The AIE API provides adaptive dataflow graphs which enables parameters to be dynamically set at runtime. However this is not supported by the VCK5000 shell and as such an alternative was required for setting the number of loop iterations at line 6. This is the reason that, for lines 2 to 5 in Listing \ref{lst:kernel_code}, four floating point numbers are read from the \emph{in\_A} stream and the first of these is extracted, cast to an integer, and used as the number of grid cells to loop over (this corresponding value has been streamed from the PL on start up). We must read the number of cells as a float because there are a maximum of two inputs and two outputs per kernel, and both inputs are required for the loading of operands.

\begin{lstlisting}[frame=lines,caption={Sketch of AIE graph building code}, label={lst:graph_code}, numbers=left]
class simpleGraph : public graph {
private:
  kernel cell_advection_kernel[3];
public:
  input_plio in_A[3], in_B[3];
  output_plio out[3];

  simpleGraph(){
    cell_advection_kernel[0]=kernel::create(cell_advection);
    ...
    in_A[0] = input_plio::create("krnl_0_in0", plio_128_bits, "data/input_A.txt");
    in_B[0]  = input_plio::create("krnl_0_in1", plio_128_bits, "data/input_B.txt");
    out[0] = output_plio::create("krnl_0_out1", plio_32_bits, "data/output_0.txt");
    ...
    for (int i=0;i<3;i++) {
      connect<stream>(in_A[i].out[0], cell_advection_kernel[i].in[0]);
      connect<stream>(in_B[i].out[0], cell_advection_kernel[i].in[1]);
      connect<stream>(cell_advection_kernel[i].out[0], out[i].in[0]);
}}};
\end{lstlisting}

This code of Listing \ref{lst:graph_code} builds the high-level AIE graph, mapping kernels to individual AI engines. Three advection kernels are created, one for each field, and lines 11-13 defines the input and output ports between the PL and AIE for the first kernel (kernels two and three are omitted for brevity). These AIE ports are connected to the kernel ports in the loop at lines 15 to 19. As described in Section \ref{sec:aie}, physical connections between AI engines are 32 bits wide, but it can be seen for input ports we specify \emph{plio\_128\_bits} at lines 11 and 12. This directs the AIE compiler that streams on the PL are 128 bits wide (of type \emph{qdma\_axis<128,0,0,0>}) and therefore data will arrive in packets of 128 bits and be unpackaged into four 32 bit stream values. The reason for this is performance, where the PL is running much slower (in our case 300MHz) compared to the AIEs (1.2 GHz) and consequently in one clock cycle the PL is providing four 32 bit numbers which the AIE will then unpack per cycle. 128 bits is the maximum width supported, and this is why in Listing \ref{lst:kernel_code} the eight numbers comprising either side of the calculation are read via two \emph{readincr\_v} calls of size four at lines 11-12 and 13-14.

\begin{table}[htb]
 \begin{center}
  \begin{tabular}{|ccc|}
    \hline
    \textbf{Version} & \makecell{\textbf{Performance} \\ \textbf{\textit{(GFLOPS)}}} & \makecell{\textbf{Compared} \\ \textbf{to PL-only}} \\
    \hline
    PL-only (no AIEs) & 14.32  & -   \\
    \hline
    Initial        & 1.99  & 14\%   \\
    Multi-kernel & 4.06 & 28\% \\
    Cascade stream & 2.78 & 19\% \\
    Cascade multiplex & 3.87 & 27\% \\
    Multi-kernel windows & 0.91 & 6\% \\
    Chunking windows & 10.32 & 72\% \\
    Reduction on host & 16.13 & 113\% \\
    Double vectorization & 18.48 & 129\% \\
  \hline
\end{tabular}
  \caption{Compute performance of different versions of AIE design compared against PL-only non-AIE implementation. All runs undertaken in single precision floating point on Xilinx VCK5000 using a problem size of 67 million grid points.}
  \label{tab:aie_performance}
 \end{center}
\end{table}

It can be seen in Listing \ref{lst:graph_code} that there is a separate kernel instance created for each of the three fields, with each of these running on a separate AI engine. Whilst the calculations for each field are different, this difference lies in the specific stencil locations that are used, and the underlying arithmetic operations are the same. Consequently we are able to reuse the same kernel code, but provide different values to these from the PL side per field. The performance of this version is reported in Table \ref{tab:aie_performance} by the \emph{initial} row, and it can be seen that this was significantly slower compared to instead undertaking all arithmetic operations on the PL (\emph{PL-only (no AIEs)}).

\subsection{Optimising the data transfer}
The maximum 128 bit width of data between the AIEs and PL was a major reason for the poor performance of our initial version reported in Table \ref{tab:aie_performance}. This was because, per cycle, the PL was only able to stream four single precision floating point numbers per stream to the AIE, whereas 24 were required (16 for the addition and 8 for the multiplication). The number of inputs to an AIE kernel is limited to two, therefore meaning that the PL could provide a maximum of eight values per cycle. Consequently three writes on each stream were required per grid cell and this conflict resulted in an initiation interval of three in our HLS code on the PL.

To address this we experimented with alternative kernel structures and, as illustrated by Figure \ref{fig:split_kernel}, split the code into multiple kernels each corresponding to a specific operation. By splitting apart the addition and multiplication, so each handles four of the eight calculations, we were able to increase the overall number of streams to six (two per kernel). There is a downside, as each individual kernel is now under utilised because it is now only undertaking four vectorized operations per cycle rather than eight, but this splitting results in six, rather than two, 128 bit streams connecting the PL to AIE kernel inputs. Consequently the HLS kernel running on the PL is able to stream the entirety of a grid cell's required data each cycle, reducing the initiation interval to one.

\begin{figure}[h]
  \centering
  \includegraphics[width=\linewidth]{images/split_kernel.png}
  \caption{Multi-kernel design, with constituent operations running across AIEs and connected by streams. Blue arrows are internal streams, green arrows are external streams between the AIEs and PL.}
  \label{fig:split_kernel}
\end{figure}

The performance of this approach is reported by the \emph{multi-kernel} row of Table \ref{tab:aie_performance}, and whilst this doubled performance compared to the initial version, it was still slower than the PL-only implementation. When undertaking profiling of our multi-kernel code using Vitis analyzer, we discovered that kernels were stalling on stream reads for over 60\% of the time. This is because, as described in Section \ref{sec:aie}, the physical streams between AI engines are 32 bits wide whereas per vectorized operation the kernel is generating 128 bits. Consequently the kernels were stalling waiting for the arrival of this data before operating upon it.

Connecting AIE kernels via cascade streams is an alternative approach and these, unlike the normal 32 bit streams, are 384 bits wide. We packed the 128 bit results into the cascade stream's \emph{accfloat} type, and streamed the entirety of the required data in one cycle. However, the limitation with cascade streams is that they physically connect between AIE cores by travelling in a horizontal manner, and when reaching the edge of a row connecting to the core above. Consequently their connection is inflexible, with each AIE core capable of only consuming cascade stream input from a single predefined neighbour and providing cascade stream output to its other neighbour. This is a problem for our multi-kernel design illustrated in Figure \ref{fig:split_kernel} as the \emph{subtraction-reduction} kernel requires inputs from two kernels, effectively requiring two cascade streams to feed into an AIE which is not possible on this architecture.

Therefore, to experiment whether cascade streams would improve performance, we adopted the design illustrated in Figure \ref{fig:cascade_kernel}, where one addition kernel undertakes all eight addition operations, and a separate kernel then undertakes the multiplication, subtraction, and reduction. The performance of this configuration is reported by \emph{cascade stream} in Table \ref{tab:aie_performance}, and the major reason for the poor performance is that the initiation interval on the PL increased to two as streams to the addition kernel require two writes per PL cycle as all eight pairs of operands are required by the single kernel. To address this we multiplexed the cascade streaming approach, with two separate copies on the AI engines such that, on average, over two clock cycles each AIE configuration receives its data. Performance of this approach is reported by the \emph{cascade multiplex} row in Table \ref{tab:aie_performance}, which improved performance but was still slower than that obtained by the non-AIE PL-only approach. Incidentally we also experimented with 4-way and 8-way multiplexing but this had no measurable improvement on performance.

\begin{figure}[h]
  \centering
  \includegraphics[scale=0.8]{images/cascade.png}
  \caption{Cascade streaming approach, Red arrow is cascade stream, green arrows are external streams to/from the PL.}
  \label{fig:cascade_kernel}
\end{figure}

To this point we have explored connecting kernels and the PL via streams, however it is also possible to use windows which provide buffers. Importantly, an AIE can read up to 256 bits per cycle from memory compared to 32 bits from streams. Therefore we reverted to our multi-kernel design of Figure \ref{fig:split_kernel} and used windows instead of streams between the kernels as well as to drive input and output data between the PL. This is illustrated in Figure \ref{fig:multi_kernel_windows} and the performance is reported as \emph{multi-kernel windows} in Table \ref{tab:aie_performance}. It can be seen that the performance was extremely poor and this is because we were operating the windows on a grid cell by grid cell basis. This meant that there was no longer a pipelined loop within each kernel because between each grid cell the kernel was stopped and restarted by the AIE graph to fill and empty the windows as required by the AIE tooling.

\begin{figure}[h]
  \centering
  \includegraphics[width=\linewidth]{images/multi-kernel-window.png}
  \caption{Multi-kernel windowing approach, coloured squares are the windows, blue connects kernels internally, and green arrows are external streams to/from the PL.}
  \label{fig:multi_kernel_windows}
\end{figure}

We modified our windowing approach to work in chunks, where data for a number of grid cells is buffered into the windows and these operate ping-pong fashion where one copy is filled with data from the producer (either the PL or another AIE kernel) whilst the other window copy is being consumed, with these switched between outer iterations of the AIE graph. Consequently our addition, multiplication, and subtraction-reduction AIE kernels are concurrently processing different chunks of grid cells based upon the data available, effectively operating as a pipeline. An added complication was that because AIE kernels are operating out of sync, for example the multiplication kernels are one chunk behind their corresponding addition kernels, this stalled the PL. This was because when streaming data to the AIEs, writes to the multiplication streams are blocked waiting for the window to become free, but this waiting on the PL also blocks writes to the addition streams which are required to progress the addition AIE kernel which will unlock its multiplication kernel. The solution was to implement explicit ping-pong buffering on the PL for the multiplication streams, with a dataflow region working in sizes of \emph{chunk} which is concurrently filling a buffer with the current chunk's data and streaming out the previous chunks data to the AIE kernel.

Performance is reported by \emph{chunking windows} in Table \ref{tab:aie_performance}, where it can be seen that this approach has significantly increased performance on the AIEs, however it is still slightly slower than the PL-only. Based on profiling via Vitis analyzer we found that the subtraction and reduction kernel was taking around double the execution time of the other kernels and this imbalance of work was causing additional stalling. Consequently we modified the kernel to perform subtraction only and streamed back to the PL 4 floating point numbers which the PL then adds together. This is reported by the row \emph{reduction on host} in Table \ref{tab:aie_performance} which outperforms the PL-only.

As described previously, in this multi-kernel approach each kernel is only working with vector sizes of four whereas the hardware is capable of undertaking eight single precision floating point operations per cycle. Working with windows, it was trivial to read two grid cells concurrently, placing the first in the lower portion of the vector and the second grid cell in the higher portion. Consequently this meant that vector operations were now running over eight operands, effectively processing two grid cells per AIE vectorized operation. This is reported by \emph{double vectorization} in Table \ref{tab:aie_performance} and resulted in a performance improvement, albeit modest as we are still limited to streaming data for only one grid cell between the PL and AIEs per cycle due to the maximum of port width of 128 bits.

\section{Multiple HLS compute units}
\label{sec:multiple}
In Section \ref{sec:aie_focus} we focused on a single PL HLS Compute Unit (CU). By decomposing across the advection problem's grid space, we can scale to multiple HLS CUs, all with a separate 3D part of the grid and working independently, connected to their own AIEs. Using our optimised AIE approach, which requires fifteen AIEs per HLS CU, we compared performance against other hardware options and Table \ref{tab:overall_performance} reports these results.

The advection kernel running on the AI engines of the VCK5000 is reported by the row \emph{VCK5000 AIEs} in Table \ref{tab:overall_performance}. Whilst not documented directly, there are a maximum of 78 128-bit PLIO input streaming interfaces that only become apparent during compilation as we scaled. This is because AIE tile contains eight 32-bit AI Engine to AXI4-Stream channels \cite{aie_architecture} and there are 39 tiles. Consequently, there are a total of 312 32-bit channels connecting the AIEs to the PL, or a maximum of 78 128-bit channels as each of these is built using four 32-bit links. Incidentally AIEs accessing DRAM directly, without the PL, would also encounter this limitation as the data still needs to traverse these same physical links.

With six input streams per field, and three fields per CU, this results in a maximum of four HLS CUs. We are therefore using 60 AIEs in total, and up to four CUs the performance scales well. Consequently this hardware restriction is a major limitation because, if we were able to scale to a greater number of CUs, then performance would likely increase significantly. The importance of streaming an entire grid cell per cycle between the PL and AIEs was highlighted in Section \ref{sec:aie_focus}, and out of the two AIE kernel designs which enable this, multi-kernel and multiplexed cascade stream, the multi-kernel is preferable in this regard as it requires six input streams per field compared to eight for the multiplexed cascade stream.

The Alveo U280 was configured with six HLS CUs, which is the maximum number that can fit due to limits on the number of ports in the Alveo shell. It can be seen that performance on the Alveo U280 is similar to that obtained on the VCK5000 using AI engines, even though there are only four CUs on the VCK5000. This is especially impressive considering that the U280 contains external HBM2 memory whereas the VCK5000 only has DDR4. We are able to fit eight CUs onto the PL-only VCK5000 configuration, which does not suffer from limitations on the number of ports due to the Versal containing a NoC which HLS kernels are connected to. The \emph{VCK5000 combined} result reports performance for a combination of the four AIE CUs with six PL-only CUs on the VCK5000, and this combined approach which leverages both the AIEs and PL for calculations delivers double the performance of the U280.

By comparison, the scheme running over the 24-core Cascade Lake Xeon Platinum CPU, which was threaded via OpenMP and compiled using GCC version 10.2 performs poorly compared with every other hardware technology. The V100 GPU version is implemented using OpenACC and version 20.9 of the Nvidia compiler, and this out-performs all other CPU and FPGA configurations, which is largely in agreement with \cite{brown2021accelerating}. Whilst the Versal has closed the gap with the GPU, it is unfortunate that AIE hardware restrictions ultimately limit the number of AIE CUs to four.

\begin{table}[htb]
 \begin{center}
  \begin{tabular}{|cc|}
    \hline
    \textbf{Description} & \textbf{Performance (GFLOPS)} \\
    \hline
    VCK5000 AIEs \textit{(4 CUs)} & 68.73 \\
    VCK5000 PL-only \textit{(8 CUs)} & 101.78 \\
    VCK5000 combined \textit{(4 and 6 CUs)} & 145.11 \\
    Alveo U280 \textit{(6 CUs)} & 72.32 \\
    24-core Xeon Platinum CPU & 23.52 \\
    V100 GPU & 227.89 \\
  \hline
\end{tabular}
  \caption{Compute performance of FPGA AIE and PL-only approaches compared to 24-core Cascade Lake CPU and Nvidia V100 GPU. All runs undertaken in single precision floating point using a problem size of 67 million grid points}
  \label{tab:overall_performance}
 \end{center}
\end{table}

\section{Conclusions and recommendations}
\label{sec:conclusions}
In this paper we have explored porting of the PW atmospheric advection scheme to the Versal, utilising the PL for tailoring memory accesses via a 3D shift-buffer and the AIEs for undertaking computation. Representative of a much wider class of stencil-based algorithms, which are popular in HPC workloads, we found that the major challenge was being able to most effectively interface the PL and AIEs to ensure data continually flows between the two. There are several possible approaches, and we have explored how hardware and tooling limitations drive specific choices and the performance impact of these. Ultimately, we found that the most effective approach was to use windows in a ping-pong fashion, working on chunks of data within the AIE kernels which rely on software pipelined loops and fully filled 8-way vectorization. Comparing against other hardware options, we found that a major limitation in obtaining performance was in the total number of streams between the PL and AIEs, which meant we were unable to scale beyond four HLS CUs. However four CUs using AIEs on the VCK5000 performed comparatively to six CUs on the Alveo U280 with the later benefiting from HBM2. The PL-only approach on the VCK5000 delivered impressive performance against the other FPGAs and CPU, which was largely due to being able to fit eight HLS CUs onto the PL, and when combining the AIEs and PL for compute we were able to deliver a significant improvement in performance compared to other FPGA approaches and the CPU. Therefore, AIEs aside, our PL-only experiments demonstrate that the Versal is a powerful architecture and improves on the Alveo.

From a development perspective there are many advantages in using the AIEs, and this will likely make the ACAP more accessible to software developers compared to traditional FPGAs. These include the overall compilation being much quicker, the ability to undertake much of the development exploration using simulation which itself is fast, no need to rebuild the PL if the interfaces between the PL and AIEs have not changed (which means bitstream regeneration takes around a minute), and the rich profiling tooling to provide insights where bottlenecks lie in the code. However it is crucial to match the workload to the architecture, and given the bandwidth between the PL and AIEs those kernels which have a higher FLOP to byte ratio than the stencil computation described in this paper will likely suit the AIEs much better. Therefore, an important lesson from this work is to focus primarily on those kernels that will not be limited by the current generation's PL to AIE interface, and algorithms with a high FLOP to byte ratio are likely where we will see the greatest benefit from this architecture.

Considering future enhancements to the Versal, in future AIE versions it would be beneficial if Xilinx were to make the physical streams between AIEs wider than 32 bits and increase the PL to AIE memory size from 128 to 256 bits, as well as supporting a larger number of PLIO streams. Increased flexibility around vector sizes would also be useful, for instance it is not possible to have a single precision floating point vector of numerous sizes including six, which required us to pad with empty values to eight, and this increased the amount of data transferred between PL and AIE. Considering the wider Vitis technology, within HLS it is not possible to create arrays of external AXI streams (e.g. of type \emph{qdma\_axis<128,0,0,0>}) and this resulted in messy code when experimenting with multiplexing. This is important because interfacing with AIEs will likely require a greater number of AXI streams compared to what is currently most common in HLS, and-so improved flexibility would be advantageous.

\section{Acknowledgements}
The authors would like to thank the ExCALIBUR H\&ES CGRA project who funded this work. We also acknowledge the ExCALIBUR H\&ES FPGA testbed and AMD Xilinx HACC program for access to compute resource used in this work, the later who also kindly provided comments and technical advice. For the purpose of open access, the author has applied a Creative Commons Attribution (CC BY) licence to any Author Accepted Manuscript version arising from this submission.

\bibliographystyle{ACM-Reference-Format}

redpajama_set_name: RedPajamaArXiv


text:
Q: Weird behavior when wrapping purrr::map within dplyr::mutate I am running into some errors I do not fully understand when trying to call purrr::map around dplyr::mutate. The reproducible code is as follows:
library(purrr)
library(dplyr)
#>
#> Attaching package: 'dplyr'
#> The following objects are masked from 'package:stats':
#>
#>     filter, lag
#> The following objects are masked from 'package:base':
#>
#>     intersect, setdiff, setequal, union
library(tibble)
# data
test_dset <- structure(list(genus = c("Aureitalea", "Aureivirga", "Auricoccucs"),
                            t_count = c(0L, 0L, 0L), n = c(1L, 1L, 1L),
                            ncbi_id = list("1176327", "1433990", character(0)),
                            g_test = list(c(`1176327` = 0),
                                          c(`1433990` = 0),
                                          structure(numeric(0), .Names = character(0)))),
                       class = c("rowwise_df", "tbl_df", "tbl", "data.frame"),
                       row.names = c(NA, -3L),
                       groups = structure(list(.rows = structure(list(1L, 2L, 3L),
                                                                 ptype = integer(0),
                                                                 class = c("vctrs_list_of","vctrs_vctr", "list"))),
                                          row.names = c(NA, -3L),
                                          class = c("tbl_df", "tbl", "data.frame")))
test_dset
#> # A tibble: 3 × 5
#> # Rowwise:
#>   genus       t_count     n ncbi_id   g_test
#>   <chr>         <int> <int> <list>    <list>
#> 1 Aureitalea        0     1 <chr [1]> <dbl [1]>
#> 2 Aureivirga        0     1 <chr [1]> <dbl [1]>
#> 3 Auricoccucs       0     1 <chr [0]> <dbl [0]>
# process a vector of pvals
proc_gtest <- function(pvals){
  if (length(pvals) == 0){
    return(NA_character_)
  }
  sig <- which(pvals < 0.05)
  if (length(sig) == 0){
    return(NA_character_)
  } else {
    return(names(pvals)[sig])
  }
}

# returns errors
test_dset |> mutate(ncbi_filt = map(g_test, proc_gtest))
#> Error: Problem with `mutate()` column `ncbi_filt`.
#> ℹ `ncbi_filt = map(g_test, proc_gtest)`.
#> ℹ `ncbi_filt` must be size 1, not 0.
#> ℹ Did you mean: `ncbi_filt = list(map(g_test, proc_gtest))` ?
#> ℹ The error occurred in row 3.
# this is okay
map(test_dset$g_test, proc_gtest)
#> [[1]]
#> [1] "1176327"
#>
#> [[2]]
#> [1] "1433990"
#>
#> [[3]]
#> [1] NA
# adding list doesn't work because it returns a list of NULL
# with names as the quantities I wanted.
test_dset |> mutate(ncbi_filt = list(map(g_test, proc_gtest))) |> pull(ncbi_filt)
#> [[1]]
#> [[1]]$`1176327`
#> NULL
#>
#>
#> [[2]]
#> [[2]]$`1433990`
#> NULL
#>
#>
#> [[3]]
#> named list()

Created on 2021-10-13 by the reprex package (v2.0.1)

Session info

sessioninfo::session_info()
#> ─ Session info ───────────────────────────────────────────────────────────────
#>  setting  value
#>  version  R version 4.1.1 (2021-08-10)
#>  os       macOS Mojave 10.14.6
#>  system   x86_64, darwin17.0
#>  ui       X11
#>  language (EN)
#>  collate  en_US.UTF-8
#>  ctype    en_US.UTF-8
#>  tz       America/New_York
#>  date     2021-10-13
#>
#> ─ Packages ───────────────────────────────────────────────────────────────────
#>  ! package     * version date       lib source
#>    assertthat    0.2.1   2019-03-21 [1] CRAN (R 4.1.0)
#>    backports     1.2.1   2020-12-09 [1] CRAN (R 4.1.0)
#>    cli           3.0.1   2021-07-17 [1] CRAN (R 4.1.0)
#>    crayon        1.4.1   2021-02-08 [1] CRAN (R 4.1.0)
#>    DBI           1.1.1   2021-01-15 [1] CRAN (R 4.1.0)
#>    digest        0.6.27  2020-10-24 [1] CRAN (R 4.1.0)
#>    dplyr       * 1.0.7   2021-06-18 [1] CRAN (R 4.1.0)
#>    ellipsis      0.3.2   2021-04-29 [1] CRAN (R 4.1.0)
#>    evaluate      0.14    2019-05-28 [1] CRAN (R 4.1.0)
#>    fansi         0.5.0   2021-05-25 [1] CRAN (R 4.1.0)
#>    fastmap       1.1.0   2021-01-25 [1] CRAN (R 4.1.0)
#>    fs            1.5.0   2020-07-31 [1] CRAN (R 4.1.0)
#>    generics      0.1.0   2020-10-31 [1] CRAN (R 4.1.0)
#>    glue          1.4.2   2020-08-27 [1] CRAN (R 4.1.0)
#>    highr         0.9     2021-04-16 [1] CRAN (R 4.1.0)
#>    htmltools     0.5.2   2021-08-25 [1] CRAN (R 4.1.0)
#>    knitr         1.34    2021-09-09 [1] CRAN (R 4.1.0)
#>    lifecycle     1.0.0   2021-02-15 [1] CRAN (R 4.1.0)
#>    magrittr      2.0.1   2020-11-17 [1] CRAN (R 4.1.0)
#>    pillar        1.6.2   2021-07-29 [1] CRAN (R 4.1.0)
#>    pkgconfig     2.0.3   2019-09-22 [1] CRAN (R 4.1.0)
#>    purrr       * 0.3.4   2020-04-17 [1] CRAN (R 4.1.0)
#>  P R.cache       0.15.0  2021-04-30 [?] CRAN (R 4.1.0)
#>  P R.methodsS3   1.8.1   2020-08-26 [?] CRAN (R 4.1.0)
#>  P R.oo          1.24.0  2020-08-26 [?] CRAN (R 4.1.0)
#>  P R.utils       2.11.0  2021-09-26 [?] CRAN (R 4.1.0)
#>    R6            2.5.1   2021-08-19 [1] CRAN (R 4.1.0)
#>    reprex        2.0.1   2021-08-05 [1] CRAN (R 4.1.0)
#>    rlang         0.4.11  2021-04-30 [1] CRAN (R 4.1.0)
#>    rmarkdown     2.11    2021-09-14 [1] CRAN (R 4.1.0)
#>    rstudioapi    0.13    2020-11-12 [1] CRAN (R 4.1.0)
#>    sessioninfo   1.1.1   2018-11-05 [3] CRAN (R 4.1.0)
#>    stringi       1.7.4   2021-08-25 [1] CRAN (R 4.1.0)
#>    stringr       1.4.0   2019-02-10 [1] CRAN (R 4.1.0)
#>  P styler        1.6.2   2021-09-23 [?] CRAN (R 4.1.0)
#>    tibble      * 3.1.4   2021-08-25 [1] CRAN (R 4.1.0)
#>    tidyselect    1.1.1   2021-04-30 [1] CRAN (R 4.1.0)
#>    utf8          1.2.2   2021-07-24 [1] CRAN (R 4.1.0)
#>    vctrs         0.3.8   2021-04-29 [1] CRAN (R 4.1.0)
#>    withr         2.4.2   2021-04-18 [1] CRAN (R 4.1.0)
#>    xfun          0.26    2021-09-14 [1] CRAN (R 4.1.0)
#>    yaml          2.2.1   2020-02-01 [1] CRAN (R 4.1.0)
#>
#> [1] /Users/quangnguyen/research/microbe_set_trait/renv/library/R-4.1/x86_64-apple-darwin17.0
#> [2] /private/var/folders/fs/hp4_8vfs665_nqytkhjc8s6w0000gn/T/Rtmp6ZA9pW/renv-system-library
#> [3] /Library/Frameworks/R.framework/Versions/4.1/Resources/library
#>
#>  P ── Loaded and on-disk path mismatch.

My understanding is that the error is due to the fact that the function being mapped returns nothing at row 3. The solution dplyr gave is that I should wrap everything in a list.
However:

*

*I am using the original map which should already return a list (other tutorials on using map to transform list columns for tibbles also did not wrap everything around list). Wrapping this inside a list returns a list of NULL elements where the things that I want to extract are set as names of this new list.

*My function does return values even if the element in the list is empty (returns NA_character_.

As seen in the reprex, the normal map function works and returns a list of length 3 with the empty row having an NA assigned to it as per the logic of the custom function. Right now I'm working around this by just generating a separate list and attach it to the data frame later, however I would love to understand what I'm looking at!

A: It is an issue with rowwise group attribute.  As we are looping over each element in map, just ungroup
library(dplyr)
library(purrr)
test_dset %>%
   ungroup %>%
   mutate(ncbi_filt = map(g_test, proc_gtest))
# A tibble: 3 × 6
  genus       t_count     n ncbi_id   g_test    ncbi_filt
  <chr>         <int> <int> <list>    <list>    <list>
1 Aureitalea        0     1 <chr [1]> <dbl [1]> <chr [1]>
2 Aureivirga        0     1 <chr [1]> <dbl [1]> <chr [1]>
3 Auricoccucs       0     1 <chr [0]> <dbl [0]> <chr [1]>

Or use map_chr to return as a vector (as there is one single value returned)
test_dset %>%
   ungroup %>%
   mutate(ncbi_filt = map_chr(g_test, proc_gtest))
# A tibble: 3 × 6
  genus       t_count     n ncbi_id   g_test    ncbi_filt
  <chr>         <int> <int> <list>    <list>    <chr>
1 Aureitalea        0     1 <chr [1]> <dbl [1]> 1176327
2 Aureivirga        0     1 <chr [1]> <dbl [1]> 1433990
3 Auricoccucs       0     1 <chr [0]> <dbl [0]> <NA>

If there is a rowwise attribute, we can directly apply the function and get the output in a list (if the output returns length > 1 or of different structure)
test_dset %>%
    mutate(ncbi_filt = list(proc_gtest(g_test)))
# A tibble: 3 × 6
# Rowwise:
  genus       t_count     n ncbi_id   g_test    ncbi_filt
  <chr>         <int> <int> <list>    <list>    <list>
1 Aureitalea        0     1 <chr [1]> <dbl [1]> <chr [1]>
2 Aureivirga        0     1 <chr [1]> <dbl [1]> <chr [1]>
3 Auricoccucs       0     1 <chr [0]> <dbl [0]> <chr [1]>

The function returns a single value, so we don't need to wrap with list as well
test_dset %>%
    mutate(ncbi_filt = proc_gtest(g_test))
# A tibble: 3 × 6
# Rowwise:
  genus       t_count     n ncbi_id   g_test    ncbi_filt
  <chr>         <int> <int> <list>    <list>    <chr>
1 Aureitalea        0     1 <chr [1]> <dbl [1]> 1176327
2 Aureivirga        0     1 <chr [1]> <dbl [1]> 1433990
3 Auricoccucs       0     1 <chr [0]> <dbl [0]> <NA>

redpajama_set_name: RedPajamaStackExchange


Farberware warrants to the original purchaser that it will replace any defective part or item which is identified and verified during the warranty period, or, at Farberware's discretion, substitute a replacement pan of similar value. This warranty does not cover damage caused by accident, misuse, abuse, commercial use or normal wear and tear. Scratches, stains, discoloration or damage from overheating are not covered by this warranty. Incidental or consequential damages are not covered by this warranty. This warranty gives you specific rights as stated herein. You may also have other rights which may vary from state to state. Should you have a problem with your cookware, please contact us via our website at Farberwarecookware.com or by phone to our Consumer Relations Department at 1-800-809-7166.
Thank you for purchasing our cookware. We wish you years of cooking enjoyment. The Farberware® cookware in this box is manufactured and inspected to ensure cooking performance. It is guaranteed to be free from defects in materials and workmanship under normal household use for the lifetime of the cookware.
Farberware Cookware Division® promises the original purchaser to replace any defective part or item during the guarantee period. If the product is out of production, a pan of similar value will be substituted.
This guarantee does not cover damage caused by accident, misuse, abuse, commercial use or normal wear and tear. Scratches, stains, discoloration, damage from overheating, or damage from use of automatic dishwasher are not covered by this guarantee. Incidental or consequential damages are not covered by this guarantee.
Should you have a problem with your cookware, please contact us via our website at FarberwareCookware.com. You may also contact our Consumer Relations Department at 1-800-809-7166.
Thank you for purchasing our cookware. We wish you years of cooking enjoyment. Farberware® is dedicated to producing great quality cookware. Farberware cookware items are manufactured and inspected to ensure a high quality. Please let us know immediately if you find any defects with your new Farberware cookware. All cookware found defective upon receipt will be replaced. Please contact us via our website at farberwarecookware.com. You may also contact our Consumer Relations Department at 1-800-809-7166.


text:
AgricultureFeatured
Project To Increase Crop Yield Launched At Egerton
A multi-agency team has been formed at Egerton University to fight soil acidity which reduces maize production on small- scale farms, raising food insecurity.
The United States Agency for International Development (USAID) through the 'Feed the Future Advancing Local Leadership, Innovation and Networks (ALL-IN)' programme has launched the effort which will be implemented by the University's Tegemeo Institute of Agricultural Policy and Development to counteract soil acidity which cuts maize yields by at least 30 per cent.
The project's Lead Principal Investigator and a research fellow at Tegemeo Institute of Agricultural Policy and Development Dr John Olwande said the initiative would involve over 4,000 farmers in counties of Bungoma, Kakamega, Trans Nzoia, and Uasin Gishu which were among the most important for maize production areas in Kenya.
"These counties are among those in which the government conducted soil tests in 2014 and found that the soils were highly acidic," Dr Olwande said.
"The project's goal is to address the high cost of soil management by involving stakeholders in the entire value chain of fertilizers and lime," he added.
While speaking at the Njoro Main Campus during the launch, the Investigator indicated that the project would bring on board experts and stakeholders to build evidence on practical ways to encourage farmers to update their knowledge about the condition of their soils and encourage them to apply appropriate soil management practices.
Dr Olwande was flanked by co-director of 'Feed the Future Advancing Local Leadership, Innovation and Networks (ALL-IN)' Dr David Sarfo Ameyaw and Director of the 'Feed the Future Innovation Lab for Markets, Risk and Resilience' Dr Michael Carter.
He stated that the project planned to engage financial institutions, farmers, agricultural experts and community actors to revitalize soils and improve yields in the region.
"Few small-scale farmers test their soils to make soil management decisions. The project will support efforts by the government, development agencies, and private sector investors in promoting and facilitating proper soil management among farmers for sustainable agricultural productivity growth," he said.
"This effort will get lime, inorganic and organic fertilizers and conservation agriculture to smallholder farmers who at present can only produce enough food to last three months, the other nine months becoming a long hungry season," he added.
The project is expected to bring together researchers, financial institutions, lime and fertilizer companies, agro-dealers, civil society organizations, extension workers, and smallholder farmers to plan ways of implementing measures aimed at solving this problem.
Dr Ameyaw observed that over 7.5 million hectares of land in Western and Central regions were highly acidic, a challenge that was being experienced in many parts of Ethiopia, Rwanda, Uganda and Zambia.
"Agriculture is critical across Sub-Saharan Africa considering the challenge of feeding and employing a rapidly growing population in the coming years. This project is a game changer in terms of improving food security in Kenya," he said.
The results from this project will provide guidance on promoting effective soil management for sustainable agricultural productivity growth in Kenya and across Sub-Saharan Africa.
By addressing widespread soil acidity, this project aligns with Feed the Future and USAID's efforts in Kenya to expand economic opportunities for smallholder farmers and entrepreneurs to build resilience of households, communities, and markets to shocks and stresses – including climate change – and diversify agricultural production," Dr Ameyaw pointed out.
He attributed the high acidity to leaching of nutrients due to abundant rains, continuous cultivation and wrong application of nitrogen fertilizers, which break down the soil forming acidic compounds.
Dr Carter noted that acidity prevents crops from utilizing the nutrients that remained in the soil, and that the application of calcium-rich lime, along with complimentary fertilizers and organic matter has been hampered by the high cost of lime, in particular the transportation cost, which kept it from reaching smallholder farmers especially in Western Kenya.
"This project will set out to test ways of relaxing the constraints that limit smallholder farmers' adoption of lime and other soil management practices that can fundamentally improve their maize yields," he said.
"The evidence from this project could even benefit smallholder farmers across the continent that faces the same challenge of soil acidity. We are about to show how a relatively small amount of resources can go a long way in solving the problem," he observed.
A research begun by Kenya Agricultural and Livestock Research Institute (KALRO) and Egerton University has demonstrated the effectiveness of lime, especially in combination with other fertilizers, for treating acidic soils.
Agribusiness: NALD's 700 hectares Integrated Farm Estate in Kogi to boost livestock value chain
Farmers: Nigeria Challenged By Insecurity, Food Security Threat

redpajama_set_name: RedPajamaCommonCrawl


text:
Q: nested for loop cross pollution I want to combine a large group of CSVs into multiple 1 million line files and somehow the file names of the CSVs are polluting my output in large groups.  There is no place where I'm echoing the $file variable so I'm confused as to why they end up in my output.
The data I'm using is checked into github
batchnum=1
ctr=1
IFS=$'\n'
for file in `ls -1 *.csv`
do
  for data in `cat $file`
  do
    echo $data >> batch${batchnum}.csv
    ctr=$[ctr + 1]
    if [ $ctr -eq 1000000 ]
    then
      batchnum=$[$batchnum + 1]
      ctr=1
  fi
  done
done

It's not really a critical issue but it has left me scratching my head.

A: Several of those CSV files have data in them that look like globs to the shell. Since your script does echo $data in an unquoted manner, you will see the contents of the entire directory expanded as globs in your output. You could fix this by using more quotes:
echo "$data" >> batch${batchnum}.csv

Which would also cause bash not to clobber whitespace. Or you could disable glob expansion:
set -f
echo $data >> batch${batchnum}.csv


The annual Spring Festival for the MFA in Creative Writing & Poetics program features readings and performances by graduating MFA candidates and a guest writer or artist. MFA candidates showcase selections from their MFA thesis projects.
Guest artist Julie Carr is the author of six books of poetry, most recently 100 Notes on Violence (2010), RAG (2014), and Think Tank (forthcoming, 2015). She is also the author of Surface Tension: Ruptural Time and the Poetics of Desire in Late Victorian Poetry (2013), and the co-editor of Active Romanticism: The Radical Impulse in Nineteenth-Century and Contemporary Poetic Practice (forthcoming, 2015). She has been the recipient of numerous awards and Honors, including The National Poetry Series and a fellowship from the National Endowment for the Arts. Julie Carr is serving as the Associate Chair for the Creative Writing Program at University of Colorado Boulder, where she is Associate Professor of English.


text:
Set sail for Wayward Tide!
by Chucklefish
We're incredibly excited to announce the latest in-house development at Chucklefish:
Wayward Tide is a co-operative top-down action adventure game, taking place in a treacherous archipelago during the golden age of piracy. You and up to three other friends will find yourselves in a bustling sea-town, ready to swash-buckle your way through five mysterious islands, of ever increasing riches (and difficulty). You'll battle dangerous enemies, hoard treasures, solve fiendish puzzles and avoid deadly traps… or maybe you'll backstab your crew to get ahead!
One of the key features we're focusing on is the nature of the puzzles and traps, which will introduce a competitive edge to the co-op experience. For instance, you and your friends might find yourself dealing with a pressure plate trap.
The solution would involve everyone stepping off at the same time. Stepping off too early will unhelpfully drop a boulder on your friend. We're currently planning on a way to resurrect co-op players from time to time, so it's not game over if a few of you get killed along the way.
Be warned, however – Death comes at a price. If all players die, they'll find themselves washed up at the starting town. The treasure will carry over, allowing them to upgrade in the town stores, but the next attempt at the island will be a different experience thanks to the randomly generated levels.
1-4 Player Support (Local and Online Co-op)
Competitive Co-op Experience
Randomly Generated levels
Five distinct islands with their own theme, enemies, puzzles and traps
Modding Support
Allow us to introduce ourselves, we're a dedicated team at Chucklefish – Working independently from the Starbound team.
SamuriFerret– Artist | Palf – Coder | Rosie – Producer | Supernorn – Artist
Community and Modding:
We're still very early in development, and there are a lot of decisions yet to be made. The vibrant community of Starbound has been a great success, and that's partly due to the modding scene. We want to encourage the same with this game, so the current plan is to make our development toolset available shortly after the game is released.
We'll be looking to the community to tell us what they want to see, so get in touch via this blog, or alternatively on Twitter (@waywardtide).
Finishing Up Starbound 1.0
Halfway "End of Summer Sale" and OST Release!
Chucklefish Presents
© Chucklefish 2022 all rights reserved


text:
"""Estimation of fidelity associated with experimental circuit executions."""
from abc import abstractmethod, ABC
from dataclasses import dataclass
from typing import (
    List,
    Optional,
    Sequence,
    Tuple,
    TYPE_CHECKING,
    Dict,
)

import numpy as np
import pandas as pd
import scipy.optimize
import scipy.stats
import sympy

from cirq import ops
from cirq.circuits import Circuit
from cirq.experiments.xeb_simulation import simulate_2q_xeb_circuits

if TYPE_CHECKING:
    import cirq
    import multiprocessing

THETA_SYMBOL, ZETA_SYMBOL, CHI_SYMBOL, GAMMA_SYMBOL, PHI_SYMBOL = sympy.symbols(
    'theta zeta chi gamma phi'
)
SQRT_ISWAP = ops.ISWAP ** 0.5

def benchmark_2q_xeb_fidelities(
    sampled_df: pd.DataFrame,
    circuits: Sequence['cirq.Circuit'],
    cycle_depths: Sequence[int],
    param_resolver: 'cirq.ParamResolverOrSimilarType' = None,
    pool: Optional['multiprocessing.pool.Pool'] = None,
):
    """Simulate and benchmark two-qubit XEB circuits.

    This uses the estimator from
    `cirq.experiments.fidelity_estimation.least_squares_xeb_fidelity_from_expectations`, but
    adapted for use on pandas DataFrames for efficient vectorized operation.

    Args:
         sampled_df: The sampled results to benchmark. This is likely produced by a call to
            `sample_2q_xeb_circuits`.
        circuits: The library of circuits corresponding to the sampled results in `sampled_df`.
        cycle_depths: The sequence of cycle depths to simulate the circuits.
        param_resolver: If circuits contain parameters, resolve according to this ParamResolver
            prior to simulation
        pool: If provided, execute the simulations in parallel.

    Returns:
        A DataFrame with columns 'cycle_depth' and 'fidelity'.
    """
    simulated_df = simulate_2q_xeb_circuits(
        circuits=circuits, cycle_depths=cycle_depths, param_resolver=param_resolver, pool=pool
    )
    df = sampled_df.join(simulated_df)

    D = 4  # two qubits
    pure_probs = np.array(df['pure_probs'].to_list())
    sampled_probs = np.array(df['sampled_probs'].to_list())
    df['e_u'] = np.sum(pure_probs ** 2, axis=1)
    df['u_u'] = np.sum(pure_probs, axis=1) / D
    df['m_u'] = np.sum(pure_probs * sampled_probs, axis=1)
    df['y'] = df['m_u'] - df['u_u']
    df['x'] = df['e_u'] - df['u_u']
    df['numerator'] = df['x'] * df['y']
    df['denominator'] = df['x'] ** 2

    def per_cycle_depth(df):
        """This function is applied per cycle_depth in the following groupby aggregation."""
        fid_lsq = df['numerator'].sum() / df['denominator'].sum()
        ret = {'fidelity': fid_lsq}

        def _try_keep(k):
            """If all the values for a key `k` are the same in this group, we can keep it."""
            if k not in df.columns:
                return  # coverage: ignore
            vals = df[k].unique()
            if len(vals) == 1:
                ret[k] = vals[0]
            else:
                # coverage: ignore
                raise AssertionError(
                    f"When computing per-cycle-depth fidelity, multiple "
                    f"values for {k} were grouped together: {vals}"
                )

        _try_keep('pair')
        return pd.Series(ret)

    if 'pair_i' in df.columns:
        groupby_names = ['layer_i', 'pair_i', 'cycle_depth']
    else:
        groupby_names = ['cycle_depth']

    return df.reset_index().groupby(groupby_names).apply(per_cycle_depth).reset_index()

class XEBCharacterizationOptions(ABC):
    @staticmethod
    @abstractmethod
    def should_parameterize(op: 'cirq.Operation') -> bool:
        """Whether to replace `op` with a parameterized version."""

    @abstractmethod
    def get_parameterized_gate(self) -> 'cirq.Gate':
        """The parameterized gate to use."""

    @abstractmethod
    def get_initial_simplex_and_names(
        self, initial_simplex_step_size: float = 0.1
    ) -> Tuple[np.ndarray, List[str]]:
        """Return an initial Nelder-Mead simplex and the names for each parameter."""

# mypy issue: https://github.com/python/mypy/issues/5374
@dataclass(frozen=True)  # type: ignore
class XEBPhasedFSimCharacterizationOptions(XEBCharacterizationOptions):
    """Options for calibrating a PhasedFSim-like gate using XEB.

    You may want to use more specific subclasses like `SqrtISwapXEBOptions`
    which have sensible defaults.

    Attributes:
        characterize_theta: Whether to characterize θ angle.
        characterize_zeta: Whether to characterize ζ angle.
        characterize_chi: Whether to characterize χ angle.
        characterize_gamma: Whether to characterize γ angle.
        characterize_phi: Whether to characterize φ angle.
        theta_default: The initial or default value to assume for the θ angle.
        zeta_default: The initial or default value to assume for the ζ angle.
        chi_default: The initial or default value to assume for the χ angle.
        gamma_default: The initial or default value to assume for the γ angle.
        phi_default: The initial or default value to assume for the φ angle.
    """

    characterize_theta: bool = True
    characterize_zeta: bool = True
    characterize_chi: bool = True
    characterize_gamma: bool = True
    characterize_phi: bool = True

    theta_default: float = 0
    zeta_default: float = 0
    chi_default: float = 0
    gamma_default: float = 0
    phi_default: float = 0

    def get_initial_simplex_and_names(
        self, initial_simplex_step_size: float = 0.1
    ) -> Tuple[np.ndarray, List[str]]:
        """Get an initial simplex and parameter names for the optimization implied by these options.

        The initial simplex initiates the Nelder-Mead optimization parameter. We
        use the standard simplex of `x0 + s*basis_vec` where x0 is given by the
        `xxx_default` attributes, s is `initial_simplex_step_size` and `basis_vec`
        is a one-hot encoded vector for each parameter for which the `parameterize_xxx`
        attribute is True.

        We also return a list of parameter names so the Cirq `param_resovler`
        can be accurately constructed during optimization.
        """
        x0 = []
        names = []
        if self.characterize_theta:
            x0 += [self.theta_default]
            names += [THETA_SYMBOL.name]
        if self.characterize_zeta:
            x0 += [self.zeta_default]
            names += [ZETA_SYMBOL.name]
        if self.characterize_chi:
            x0 += [self.chi_default]
            names += [CHI_SYMBOL.name]
        if self.characterize_gamma:
            x0 += [self.gamma_default]
            names += [GAMMA_SYMBOL.name]
        if self.characterize_phi:
            x0 += [self.phi_default]
            names += [PHI_SYMBOL.name]

        x0 = np.asarray(x0)
        n_param = len(x0)
        initial_simplex = [x0]
        for i in range(n_param):
            basis_vec = np.eye(1, n_param, i)[0]
            initial_simplex += [x0 + initial_simplex_step_size * basis_vec]
        initial_simplex = np.asarray(initial_simplex)

        return initial_simplex, names

    def get_parameterized_gate(self):
        theta = THETA_SYMBOL if self.characterize_theta else self.theta_default
        zeta = ZETA_SYMBOL if self.characterize_zeta else self.zeta_default
        chi = CHI_SYMBOL if self.characterize_chi else self.chi_default
        gamma = GAMMA_SYMBOL if self.characterize_gamma else self.gamma_default
        phi = PHI_SYMBOL if self.characterize_phi else self.phi_default
        return ops.PhasedFSimGate(theta=theta, zeta=zeta, chi=chi, gamma=gamma, phi=phi)

@dataclass(frozen=True)
class SqrtISwapXEBOptions(XEBPhasedFSimCharacterizationOptions):
    """Options for calibrating a sqrt(ISWAP) gate using XEB.

    As such, the default for theta is changed to -pi/4 and the parameterization
    predicate seeks out sqrt(ISWAP) gates.
    """

    theta_default: float = -np.pi / 4

    @staticmethod
    def should_parameterize(op: 'cirq.Operation') -> bool:
        return op.gate == SQRT_ISWAP

def parameterize_circuit(
    circuit: 'cirq.Circuit',
    options: XEBCharacterizationOptions,
) -> 'cirq.Circuit':
    """Parameterize PhasedFSim-like gates in a given circuit according to
    `phased_fsim_options`.
    """
    gate = options.get_parameterized_gate()
    return Circuit(
        ops.Moment(
            gate.on(*op.qubits) if options.should_parameterize(op) else op
            for op in moment.operations
        )
        for moment in circuit.moments
    )

QPair_T = Tuple['cirq.Qid', 'cirq.Qid']

@dataclass(frozen=True)
class XEBCharacterizationResult:
    """The result of `characterize_phased_fsim_parameters_with_xeb`.

    Attributes:
        optimization_results: A mapping from qubit pair to the raw scipy OptimizeResult object
        final_params: A mapping from qubit pair to a dictionary of (angle_name, angle_value)
            key-value pairs
        fidelities_df: A dataframe containing per-cycle_depth and per-pair fidelities after
            fitting the characterization.
    """

    optimization_results: Dict[QPair_T, scipy.optimize.OptimizeResult]
    final_params: Dict[QPair_T, Dict[str, float]]
    fidelities_df: pd.DataFrame

def characterize_phased_fsim_parameters_with_xeb(
    sampled_df: pd.DataFrame,
    parameterized_circuits: List['cirq.Circuit'],
    cycle_depths: Sequence[int],
    options: XEBCharacterizationOptions,
    initial_simplex_step_size: float = 0.1,
    xatol: float = 1e-3,
    fatol: float = 1e-3,
    verbose: bool = True,
    pool: Optional['multiprocessing.pool.Pool'] = None,
) -> XEBCharacterizationResult:
    """Run a classical optimization to fit phased fsim parameters to experimental data, and
    thereby characterize PhasedFSim-like gates.

    Args:
        sampled_df: The DataFrame of sampled two-qubit probability distributions returned
            from `sample_2q_xeb_circuits`.
        parameterized_circuits: The circuits corresponding to those sampled in `sampled_df`,
            but with some gates parameterized, likely by using `parameterize_circuit`.
        cycle_depths: The depths at which circuits were truncated.
        options: A set of options that controls the classical optimization loop
            for characterizing the parameterized gates.
        initial_simplex_step_size: Set the size of the initial simplex for Nelder-Mead.
        xatol: The `xatol` argument for Nelder-Mead. This is the absolute error for convergence
            in the parameters.
        fatol: The `fatol` argument for Nelder-Mead. This is the absolute error for convergence
            in the function evaluation.
        verbose: Whether to print progress updates.
        pool: An optional multiprocessing pool to execute circuit simulations in parallel.
    """
    (pair,) = sampled_df['pair'].unique()
    initial_simplex, names = options.get_initial_simplex_and_names(
        initial_simplex_step_size=initial_simplex_step_size
    )
    x0 = initial_simplex[0]

    def _mean_infidelity(angles):
        params = dict(zip(names, angles))
        if verbose:
            params_str = ''
            for name, val in params.items():
                params_str += f'{name:5s} = {val:7.3g} '
            print(f"Simulating with {params_str}")
        fids = benchmark_2q_xeb_fidelities(
            sampled_df, parameterized_circuits, cycle_depths, param_resolver=params, pool=pool
        )

        loss = 1 - fids['fidelity'].mean()
        if verbose:
            print(f"Loss: {loss:7.3g}", flush=True)
        return loss

    optimization_result = scipy.optimize.minimize(
        _mean_infidelity,
        x0=x0,
        options={'initial_simplex': initial_simplex, 'xatol': xatol, 'fatol': fatol},
        method='nelder-mead',
    )

    final_params = dict(zip(names, optimization_result.x))
    fidelities_df = benchmark_2q_xeb_fidelities(
        sampled_df, parameterized_circuits, cycle_depths, param_resolver=final_params
    )
    return XEBCharacterizationResult(
        optimization_results={pair: optimization_result},
        final_params={pair: final_params},
        fidelities_df=fidelities_df,
    )

@dataclass(frozen=True)
class _CharacterizePhasedFsimParametersWithXebClosure:
    """A closure object to wrap `characterize_phased_fsim_parameters_with_xeb` for use in
    multiprocessing."""

    parameterized_circuits: List['cirq.Circuit']
    cycle_depths: Sequence[int]
    options: XEBCharacterizationOptions
    initial_simplex_step_size: float = 0.1
    xatol: float = 1e-3
    fatol: float = 1e-3

    def __call__(self, sampled_df) -> XEBCharacterizationResult:
        return characterize_phased_fsim_parameters_with_xeb(
            sampled_df=sampled_df,
            parameterized_circuits=self.parameterized_circuits,
            cycle_depths=self.cycle_depths,
            options=self.options,
            initial_simplex_step_size=self.initial_simplex_step_size,
            xatol=self.xatol,
            fatol=self.fatol,
            verbose=False,
            pool=None,
        )

def characterize_phased_fsim_parameters_with_xeb_by_pair(
    sampled_df: pd.DataFrame,
    parameterized_circuits: List['cirq.Circuit'],
    cycle_depths: Sequence[int],
    options: XEBCharacterizationOptions,
    initial_simplex_step_size: float = 0.1,
    xatol: float = 1e-3,
    fatol: float = 1e-3,
    pool: Optional['multiprocessing.pool.Pool'] = None,
) -> XEBCharacterizationResult:
    """Run a classical optimization to fit phased fsim parameters to experimental data, and
    thereby characterize PhasedFSim-like gates grouped by pairs.

    This is appropriate if you have run parallel XEB on multiple pairs of qubits.

    The optimization is done per-pair. If you have the same pair in e.g. two different
    layers the characterization optimization will lump the data together. This is in contrast
    with the benchmarking functionality, which will always index on `(layer_i, pair_i, pair)`.

    Args:
        sampled_df: The DataFrame of sampled two-qubit probability distributions returned
            from `sample_2q_xeb_circuits`.
        parameterized_circuits: The circuits corresponding to those sampled in `sampled_df`,
            but with some gates parameterized, likely by using `parameterize_circuit`.
        cycle_depths: The depths at which circuits were truncated.
        options: A set of options that controls the classical optimization loop
            for characterizing the parameterized gates.
        initial_simplex_step_size: Set the size of the initial simplex for Nelder-Mead.
        xatol: The `xatol` argument for Nelder-Mead. This is the absolute error for convergence
            in the parameters.
        fatol: The `fatol` argument for Nelder-Mead. This is the absolute error for convergence
            in the function evaluation.
        pool: An optional multiprocessing pool to execute pair optimization in parallel. Each
            optimization (and the simulations therein) runs serially.
    """
    pairs = sampled_df['pair'].unique()
    closure = _CharacterizePhasedFsimParametersWithXebClosure(
        parameterized_circuits=parameterized_circuits,
        cycle_depths=cycle_depths,
        options=options,
        initial_simplex_step_size=initial_simplex_step_size,
        xatol=xatol,
        fatol=fatol,
    )
    subselected_dfs = [sampled_df[sampled_df['pair'] == pair] for pair in pairs]
    if pool is not None:
        results = pool.map(closure, subselected_dfs)
    else:
        results = [closure(df) for df in subselected_dfs]

    optimization_results = {}
    all_final_params = {}
    fid_dfs = []
    for result in results:
        optimization_results.update(result.optimization_results)
        all_final_params.update(result.final_params)
        fid_dfs.append(result.fidelities_df)

    return XEBCharacterizationResult(
        optimization_results=optimization_results,
        final_params=all_final_params,
        fidelities_df=pd.concat(fid_dfs),
    )

def exponential_decay(cycle_depths: np.ndarray, a: float, layer_fid: float) -> np.ndarray:
    """An exponential decay for fitting.

    This computes `a * layer_fid**cycle_depths`

    Args:
        cycle_depths: The various depths at which fidelity was estimated. This is the independent
            variable in the exponential function.
        a: A scale parameter in the exponential function.
        layer_fid: The base of the exponent in the exponential function.
    """
    return a * layer_fid ** cycle_depths

def _fit_exponential_decay(
    cycle_depths: np.ndarray, fidelities: np.ndarray
) -> Tuple[float, float, float, float]:
    """Fit an exponential model fidelity = a * layer_fid**x using nonlinear least squares.

    This uses `exponential_decay` as the function to fit with parameters `a` and `layer_fid`.

    Args:
        cycle_depths: The various depths at which fidelity was estimated. Each element is `x`
            in the fit expression.
        fidelities: The estimated fidelities for each cycle depth. Each element is `fidelity`
            in the fit expression.

    Returns:
        a: The first fit parameter that scales the exponential function, perhaps accounting for
            state prep and measurement (SPAM) error.
        layer_fid: The second fit parameters which serves as the base of the exponential.
        a_std: The standard deviation of the `a` parameter estimate.
        layer_fid_std: The standard deviation of the `layer_fid` parameter estimate.
    """
    cycle_depths = np.asarray(cycle_depths)
    fidelities = np.asarray(fidelities)

    # Get initial guess by linear least squares with logarithm of model.
    # This only works for positive fidelities. We use numpy fancy indexing
    # with `positives` (an ndarray of bools).
    positives = fidelities > 0
    if np.sum(positives) <= 1:
        # The sum of the boolean array is the number of `True` entries.
        # For one or fewer positive values, we cannot perform the linear fit.
        return 0, 0, np.inf, np.inf
    cycle_depths_pos = cycle_depths[positives]
    log_fidelities = np.log(fidelities[positives])
    slope, intercept, _, _, _ = scipy.stats.linregress(cycle_depths_pos, log_fidelities)
    layer_fid_0 = np.clip(np.exp(slope), 0, 1)
    a_0 = np.clip(np.exp(intercept), 0, 1)

    try:
        (a, layer_fid), pcov = scipy.optimize.curve_fit(
            exponential_decay,
            cycle_depths,
            fidelities,
            p0=(a_0, layer_fid_0),
            bounds=((0, 0), (1, 1)),
        )
    except ValueError:  # coverage: ignore
        # coverage: ignore
        return 0, 0, np.inf, np.inf

    a_std, layer_fid_std = np.sqrt(np.diag(pcov))
    return a, layer_fid, a_std, layer_fid_std

def _one_unique(df, name, default):
    """Helper function to assert that there's one unique value in a column and return it."""
    if name not in df.columns:
        return default
    vals = df[name].unique()
    assert len(vals) == 1, name
    return vals[0]

def fit_exponential_decays(fidelities_df: pd.DataFrame) -> pd.DataFrame:
    """Fit exponential decay curves to a fidelities DataFrame.

    Args:
         fidelities_df: A DataFrame that is the result of `benchmark_2q_xeb_fidelities`. It
            may contain results for multiple pairs of qubits identified by the "pair" column.
            Each pair will be fit separately. At minimum, this dataframe must contain
            "cycle_depth", "fidelity", and "pair" columns.

    Returns:
        A new, aggregated dataframe with index given by (pair, layer_i, pair_i); columns
        for the fit parameters "a" and "layer_fid"; and nested "cycles_depths" and "fidelities"
        lists (now grouped by pair).
    """

    def _per_pair(f1):
        a, layer_fid, a_std, layer_fid_std = _fit_exponential_decay(
            f1['cycle_depth'], f1['fidelity']
        )
        record = {
            'a': a,
            'layer_fid': layer_fid,
            'cycle_depths': f1['cycle_depth'].values,
            'fidelities': f1['fidelity'].values,
            'a_std': a_std,
            'layer_fid_std': layer_fid_std,
        }
        return pd.Series(record)

    if 'layer_i' in fidelities_df.columns:
        groupby = ['layer_i', 'pair_i', 'pair']
    else:
        groupby = ['pair']
    return fidelities_df.groupby(groupby).apply(_per_pair)

def before_and_after_characterization(
    fidelities_df_0: pd.DataFrame, characterization_result: XEBCharacterizationResult
) -> pd.DataFrame:
    """A convenience function for horizontally stacking results pre- and post- characterization
    optimization.

    Args:
        fidelities_df_0: A dataframe (before fitting), likely resulting from
            `benchmark_2q_xeb_fidelities`.
        characterization_result: The result of running a characterization. This contains the
            second fidelities dataframe as well as the new parameters.

    Returns:
          A joined dataframe with original column names suffixed by "_0" and characterized
          column names suffixed by "_c".
    """
    fit_decay_df_0 = fit_exponential_decays(fidelities_df_0)
    fit_decay_df_c = fit_exponential_decays(characterization_result.fidelities_df)

    joined_df = fit_decay_df_0.join(fit_decay_df_c, how='outer', lsuffix='_0', rsuffix='_c')
    # Remove (layer_i, pair_i) from the index. While we keep this for `fit_exponential_decays`
    # so the same pair can be benchmarked in different contexts, the multi-pair characterization
    # function only keys on the pair identity. This can be seen acutely by the
    # `characterization_result.final_params` dictionary being keyed only by the pair.
    joined_df = joined_df.reset_index().set_index('pair')

    joined_df['characterized_angles'] = [
        characterization_result.final_params[pair] for pair in joined_df.index
    ]
    # Take any `final_params` (for any pair). We just need the angle names.
    fp, *_ = characterization_result.final_params.values()
    for angle_name in fp.keys():
        joined_df[angle_name] = [
            characterization_result.final_params[pair][angle_name] for pair in joined_df.index
        ]
    return joined_df

redpajama_set_name: RedPajamaGithub


text:
Weekly film reviews. by Howard Feinstein
Doctor's Order: The Innocents
by Howard Feinstein
in Columns
Agata Buzek, Agata Kulesza, Anne Fontaine, Caroline Champetier, Lou de Laage, Pascal Bonitzer, The Innocents, Vincent Macaigne
Determinism or free will? I'm flummoxed. This is my second successive review of a film about nuns. The first was Zach Clark's Little Sister, in which meek ex-goth Colleen Lunsford (Addison Timlin) is a novice in a New York City convent whose mother superior, like the newcomer herself, doubts the young woman's faith and commitment to the order of the Sisters of Mercy. During a trip to the family home in North Carolina — half therapy, half reunion with a brother mutilated from combat — she appropriates the flamboyance and kitsch that had been a substantial part of their youth.
A nun's life choices and penguin-like clothing have often been the butt of camp humor. Halloween and costume parties often have several in attendance, some sporting facial hair. The real things are collective foils, their perceived commonality intrinsic to other, non-comic stereotypes — like the ones at play in French director Anne Fontaine's superb The Innocents, the subject of this second critique.
Lou de Laage, Agata Buzek
In this case, the nuns, osoby, in an isolated Polish Benedictine convent at the close of World War Two are unlikely objects of sacrilegious jokes. They are sober, obedient women desperately trying to hold on to their faith after enduring mass rapes and numerous impregnations by incoming Russian soldiers who quickly replaced the German occupiers. Fontaine probes, reaching beyond their nearly identical black habits and shared routines to uncover distinctive characteristics. For example, each of the women, who have vowed chastity but are in a family way against their will, reacts with marked difference to the condition, treatment, labor, and, ultimately, the child itself.
What sets these damaged women apart from the larger society outside their strict rule-bound world of devotion are unencouraged brushes with secularism, both vulgar, i.e., the bestial grunts who considered Polish women their prizes after their military victory, and innocuous, sometimes courageous. Among the latter are acts that raise the roadblock granting access on a two-way street of reciprocity. French Red Cross doctor Madeleine Pauliac fell into the latter category, in spades. The Innocents recounts only a small portion of her heroic life, tragically cut short in an accident right after the war. In Poland, she aided, often saved, all kinds of women who had suffered collective rape — in convents and maternity wards — as well as individual violation.
Agata Kulesza, center
Illustrating the secular-versus-spiritual dynamic at its most original and creatively intricate is a scene in which the one nun with a past, French-speaking Sister Maria (Agata Buzek) — the gentle second-in-command to the order's abbess, the siostra przelozona, whose rigid reign is frightening — gives the print dress she wore the day she entered the convent to a beautiful young French Red Cross doctor, Mathilde (an excellent Lou de Laage). Mathilde, based on Pauliac, needs a change of clothes to maintain her cover while she singlehandedly, against military regulations (her assignment is to aid only French victims of the war), attends daily to these so-called "brides of Christ." Sister Maria's nostalgia is palpable as she unpacks the lovely outfit and recalls her earlier life to the physician, with whom she forms a strong bond. The material item returns to the secular world after several years of incubation in a sacred place.
More is happening here than meets the eye. Mathilde has been decked out in drab military pants and jacket, her hair pulled back tightly. You get the feeling that Sister Maria is sharing a palatable femininity — less that which emerges when the order's members walk in single file, white wimples snugly attached to shoulders and necks, black habits flowing effortlessly against the building's crumbling, light-colored plaster façade; or sit in horizontal rows in the chapel, chanting chorales in perfect harmony — than a surprisingly secular expression of womanhood, an offering from a nun so appreciative of the outsider's charitable work that she presents it in a way to which both can relate.
The dress's transitional role in Sister Maria's life adds both an intimacy and a recognition of overlapping values in spite of their opposing philosophical takes on life. The gift is heartfelt. Communist and atheist Mathilde's decision to risk her career and her life for the well-being of the righteous Poles with whom she cannot even communicate — she subscribes to the secular Socialist ideal of a better world — echoes the leap of faith that Sister Maria completed when she committed herself to the insular spiritual domain. To be sure, this nun was no pushover. "24 hours of doubt and one minute of hope," she says of her devotion.
Neither Fontaine nor DP Caroline Champetier tries to impose a design alien to the Benedictine belief system to circumscribe the deceptively complex lives of these women and the horrid acts that have provoked a questioning of faith. The artists respectfully let the nuns lead the way; the camera discreetly follows them with subtle tilts and pans. A graceful symmetry prevails.
Tracking shots of the sisters are smooth, the camera occasionally pausing for a two-shot of an iconographic bust and the face of pious Sister Maria or the more earthbound Mathilde, or at times just of the visages of the two. These profound still lives owe much to Bergmanesque arrangements in films like Persona but go further: They invite spectators to meditate upon their juxtapositions, to have at their disposal concrete images to use in tackling difficult, abstract metaphysical issues.
The shapes of the cross and the gothic arch are essential for translating the spiritual into something comprehensible to humans. Sculpted crucifixes outdoors, in the snowy forest that abuts the convent, end up being mixed-signal signifiers, while wooden ones adorning interior walls, like décor that has been blessed, are there to provide comfort.
At the end of the day, however, the cross exists more in the mind, a spatial entity realized verbally. "Behind all joy lies the cross," Sister Maria tells Mathilde. "Help me bear this heavy cross," says the abbess (Agata Kulesza, the Stalinist aunt in Ida and one of the finest actors working in cinema today), whose questionable actions during and after the impregnation crisis can't help but gnaw at her conscience.
The arch is less burdened by dogma. As an architectural element — windows in two dimensions, vaulted ceilings in three — it surpasses the rounded Romanesque arch of an earlier time in height and capacity for structural support. Of course, it is a metaphor for aspiration. Increased verticality gets you closer to God.
Mathilde is at the center of the story; in fact, the film is presented entirely from her point of view. It is not easy to create, or in this case redefine, an ordinary mortal. But this nonbeliever from the working class with her own moral sensibility is special, a woman who alone services other females who have the same religion-based value system. They have taken the veil instead of Mathilde's choices of the material and the male. A quick thinker, she comes up with imaginative false solutions to precarious situations, such as a potential follow-up attack by the Russians or what to do with unwanted babies.
Vincent Macaigne, Lou de Laage
Like a devout Buddhist, Mathilde prizes the ethics of the day-to-day, especially regarding the treatment of others. She had become a doctor after volunteering to carry the wounded on stretchers in wartime Paris. She is the most sympathetic of her colleagues toward the homeless urchins who sleep at the entrance of the Red Cross office. And it is she who takes action when a distressed nun from the convent, who is also bypassing the rules to which she is bound, begs and prays for medical attention for a sister undergoing a cruel, hush-hush labor.
Mathilde is the only female who engages in any activity that could be considered playful, like trips to the local café for drinks and music, followed by casual sex, with Samuel (Vincent Macaigne, outstanding), the older Jewish doctor for whom she serves as an overqualified operating assistant. (Quick digression: She is in charge only when she begins to work the all-female convent.) The infrequent visits to the café, where tablecloths are checkered and polkas played on the accordion, do provide welcome relief from the surrounding grimness. These are among the few scenes in which Mathilde literally lets her hair down, the butch military style giving way to a sensuous 'do, but they are also well timed ruptures in the narrative that help maintain a consistent rhythm.
The development of the characters of the anachronistic, middle-aged abbess and most of the seven expectant nuns is a study in authoritarianism versus empowerment, an exercise in extreme yin and yang that is as relevant today as it was in 1945. Fontaine views the girls on a case-by-case basis, documenting their different reactions to the dilemma at hand and the actions that follow. One girl is full of warmth and love, another in almost pathological denial. As a group, however, they are a three-dimensional construct.
The abbess, on the other hand, lacks depth. So literal is she in interpreting dogma, so overprotective of the order's stature, that she thinks with the narrowest of templates, in the process lying and endangering the women in her charge. Their recent sojourn in hell does not factor in to her non-negotiable decisions. Such extreme behavior is not unlike some of the arrogant, selfish actions we associate today with fundamentalism at its most twisted.
Samuel occupies an important place in the narrative, especially in the elaboration of difference between Sister Maria and Mathilde. An intellectual born into privilege, he is by nature a sarcastic, funny cynic. His negativity has become so exacerbated by the loss of both his parents at Bergen-Belsen that he can not believe in anything. He functions as Point Zero in the friendly rivalry between the two women in matters of faith. Through the passive-aggressive Samuel and Macaigne's extraordinary performance, the film finds a clever, unforced way to include the oft-addressed horrors suffered in Poland by the Jews.
Fontaine and veteran screenwriter Pascal Bonitzer have written a methodical script in which nothing is accidental; events are signaled by the slightest, most delicate images. For example, the placement of the street kids at the very beginning of the film plants the seed for the revelation of newborns at the convent. Or, on one of the evenings Samuel spends with Mathilde at the café, the self-pitying fellow cheapens their relationship when he asks if she thinks they will recall each other in the future only with post cards. The comment lays the groundwork for a conclusion–all right, a post card is one of many elements involved–that might point to a ray of light following all the grotesqueness, but could also be construed as yet another chapter of darkness. Such ambiguity is appropriate for a film set at the end of a world war, but at the beginning of another: the Cold War. The Innocents is that rare picture that captures the weight of history without losing sight of the individuals who live it.
The Oscars Honors Imagineer Systems' Mocha By Michael Murie on Feb 22, 2013
Director Leos Carax on Holy Motors By Scott Macaulay on Oct 22, 2012
25 New Faces Update: At SXSW and Beyond By Nick Dawson on Feb 7, 2013


Marcus Antonius said, that the greatness of the people of Rome was not so much seen in what they took, as in what they gave; and, indeed, some ages before Antonius, they had dethroned one amongst the rest with so wonderful authority, that in all the Roman history I have not observed anything that more denotes the height of their power. Antiochus possessed all Egypt, and was, moreover, ready to conquer Cyprus and other appendages of that empire: when being upon the progress of his victories, C. Popilius came to him from the Senate, and at their first meeting refused to take him by the hand, till he had first read his letters, which after the king had read, and told him he would consider of them, Popilius made a circle about him with his cane, saying:—"Return me an answer, that I may carry it back to the Senate, before thou stirrest out of this circle." Antiochus, astonished at the roughness of so positive a command, after a little pause, replied, "I will obey the Senate's command." Then Popilius saluted him as friend of the Roman people. To have renounced claim to so great a monarchy, and a course of such successful fortune, from the effects of three lines in writing! Truly he had reason, as he afterwards did, to send the Senate word by his ambassadors, that he had received their order with the same respect as if it had come from the immortal gods.
All the kingdoms that Augustus gained by the right of war, he either restored to those who had lost them or presented them to strangers. And Tacitus, in reference to this, speaking of Cogidunus, king of England, gives us, by a marvellous touch, an instance of that infinite power: the Romans, says he, were from all antiquity accustomed to leave the kings they had subdued in possession of their kingdoms under their authority.
'Tis probable that Solyman, whom we have seen make a gift of Hungary and other principalities, had therein more respect to this consideration than to that he was wont to allege, viz., that he was glutted and overcharged with so many monarchies and so much dominion, as his own valour and that of his ancestors had acquired.


text:
Le Foundling Hospital est une ancienne institution londonienne fondée en 1739 par l'officier de marine et philanthrope Thomas Coram pour recueillir les enfants abandonnés en vue de leur assurer soin, éducation et placement. L'institution, après avoir déménagé dans les années 1920, disparaît en 1951. Ce qui reste du Foundling Hospital est devenu depuis un musée (le Foundling Museum) et une fondation, qui n'ont plus rien à voir avec l'établissement d'origine.

Entre-temps, l'orphelinat devient la première galerie d'art jamais ouverte en Angleterre : les œuvres exposées  dans les années 1740-1760, ainsi que les concerts organisés, permirent de mieux financer l'établissement et d'attirer plus de dons et c'est sans doute l'une des premières fois où sont associés arts et soins en Occident, et l'invention du charity business.

Histoire

Les premiers enfants furent admis le , en un lieu temporaire, une maison située à Hatton Garden. On ne posait alors aucune question concernant l'enfant ou les parents mais souvent les parents déposaient avec l'enfant un signe distinctif. Il s'agissait le plus souvent de pièces marquées, de babioles, de morceaux d'étoffe ou de rubans, de vers écrit sur un morceau de papier. Ces signes et les vêtements (si l'enfant en portait) étaient soigneusement enregistrés. L'une de ces inscriptions disait par exemple, . Comme les enfants déposés se faisaient plus nombreux, on mit au point un système de tirage au sort, constitué de boules rouges, blanches et noires.

Les enfants, n'étaient que rarement admis après l'âge de douze mois. À leur arrivée, ils étaient envoyés chez une nourrice à la campagne, où ils restaient jusqu'à l'âge de quatre ou cinq ans, avant de revenir à l'hôpital. À seize ans, les filles étaient généralement apprenties domestique pour quatre années ; à quatorze ans les garçons étaient apprentis dans divers métiers pour sept années.

En septembre 1742, la première pierre du nouvel Hospital fut posée en un lieu nommé Bloomsbury, au nord de Great Ormond Street et à l'ouest de Gray's Inn Lane. Les plans du nouveau bâtiment sont dessinés par l'architecte Theodore Jacobsen comme un bâtiment en briques pleines comportant deux ailes et une chapelle, construites autour d'une vaste cour. L'aile ouest fut achevée en octobre 1745. L'aile est fut ajoutée en 1752 . Le nouvel Hospital fut décrit comme  et devint l'œuvre de charité la plus populaire de Londres, du fait que certains artistes s'y intéressèrent.

Les arts au service des enfants

L'idée d'introduire des œuvres d'art dans le bâtiment afin d'attirer de riches donateurs et d'en reverser une partie du produit des ventes à la direction de l'orphelinat est le fait de William Hogarth, qui n'avait lui-même pas d'enfant. Au début des années 1740, l'endroit sensibilise des artistes. Ainsi, en , le musicien Georg Friedrich Haendel est nommé gouverneur et y donne des concerts de charité dans la chapelle, afin de financer le nouveau lieu. L'endroit devient progressivement à la mode, ou pour le moins fréquentable : le but était de placer des enfants dans de riches familles, de collecter toujours plus de dons. Le Parlement britannique avait en effet été alarmé par l'augmentation des coûts d'entretien : outre le budget prévisionnel du nouveau bâtiment, couvert par une dotation spéciale et des événements comme le concert de Haendel, les frais de fonctionnement s'élevaient en 1749 à plus d'un demi million de £, une somme considérable à l'époque, pour s'occuper de près de  orphelins. Sur ce nombre, seulement 30 % survivaient, le taux de mortalité infantile étant à cette époque considérable.

Hogarth va donc organiser dans l'une des salles du nouveau bâtiment une série d'expositions, qui devient historiquement le premier espace dédié en Angleterre à l'art. Ces expositions préfigurent celles de la Society of Artists instituée en 1760, puis celles de la Royal Academy à partir de 1768.

Hogarth fit un grand portrait du capitaine Coram en 1740. Il dessina les uniformes du personnel et le blason de l'établissement. Il fit venir de nombreux confrères, ceux qu'il côtoyait dans les gentlemen's clubs, des sociétés d'artistes, d'érudits et d'aristocrates collectionneurs.

Les murs du Foundling Hospital se couvrent bientôt de tableaux de Joshua Reynolds, Thomas Gainsborough, Richard Wilson, Francis Hayman, ou encore Benjamin West, qui conçut une pièce pour l'autel de la chapelle. Certaines toiles furent offertes et d'autres, gagnées par des membres de l'institution à la loterie. L'organisation des expositions fut prise en charge un temps par la Society of Dilettanti. La plupart de ces œuvres, inestimables, sont exposées au Foundling Museum.

La maltraitance
Un épisode tragique illustre la maltraitance réservée aux enfants à cette époque : l'affaire Elizabeth Brownrigg (1720–1767). Employée comme infirmière, elle frappait à mort de nombreux jeunes enfants au point qu'une plainte fut déposée devant le Old Bailey, et, le juge, convaincu, la fit pendre à Tyburn.

Nouveaux bâtiments, fermeture et transformations
Dans les années 1920, les enfants et le personnel sont déplacés à la campagne, dans un ancien couvent situé à Redhill (Surrey), et ce, pour des raisons sanitaires, pour finir à Berkhamsted. Les anciens bâtiments londoniens, immenses, devaient un temps servir à remplacer les halles de Covent Garden, le marché central de Londres. Les terrains sont peu à peu cédés à des promoteurs au bénéfice de l'orphelinat. En 1951, une loi britannique met fin au système des orphelinats. Une partie du terrain avait été transformée en parc pour les enfants, une autre servit à construire en 1937 un centre d'éducation scolaire, puis, devint le local du Foundling Museum, attenant à la , laquelle existe toujours.

Références

Bibliographie
   [article] « Foundling Hospitals », In: Hugh Chisholm (éditeur), Encyclopædia Britannica, volume 10, Cambridge University Press, 1911,   — en ligne.

Articles connexes
 Abandon d'enfants

Centre hospitalier à Londres
Orphelinat
Organisme disparu en 1951

redpajama_set_name: RedPajamaWikipedia