diff --git a/Christmas/2022/README.md b/Christmas/2022/README.md new file mode 100644 index 0000000000000000000000000000000000000000..240f80853b1ee0579d7b5537dae9cb49bae642c6 --- /dev/null +++ b/Christmas/2022/README.md @@ -0,0 +1,22 @@ +--- +title: Christmas Tree 2022 +author: An-Chi Ho +date: Christmas Season 2022 +--- + +Plot your own Christmas tree by R base functions and spice up with an emoji twist! + +* Inspired by https://github.com/R-CoderDotCom/christmas-tree +* Package "emo": https://github.com/hadley/emo (make sure it is installed) +* Colors reference: + - https://www.rapidtables.com/web/color/RGB_Color.html + - https://www.datanovia.com/en/blog/awesome-list-of-657-r-color-names/ + +**[Steps]** + +1. Get all the code and recipe from Rsenal GitLab repo https://earth.bsc.es/gitlab/es/rsenal/-/tree/main/Christmas/2022/ +2. Modify the recipe +3. Run plot_ChristmasTree.R +4. Share your tree with us! + +Note that saving into a file is faster than plotting in a pop-up window. diff --git a/Christmas/2023/ChristmasTree.R b/Christmas/2023/ChristmasTree.R new file mode 100755 index 0000000000000000000000000000000000000000..bdd4956aae641c604eb223906d905117009ab745 --- /dev/null +++ b/Christmas/2023/ChristmasTree.R @@ -0,0 +1,184 @@ +ChristmasTree <- function(recipe) { + +# Config +author <- recipe$Description$Title$author +title <- recipe$Description$Title$text +title.col <- recipe$Description$Title$color +bg.col <- strsplit(recipe$background$color, ' ')[[1]] +tree.col <- strsplit(recipe$tree$leaves, ' ')[[1]] +trunk.col <- strsplit(recipe$tree$trunk, ' ')[[1]] +ball.col <- strsplit(recipe$balls$color, ' ')[[1]] +snow.col <- strsplit(recipe$snow$color, ' ')[[1]] +snow.level <- recipe$snow$level +star <- recipe$star$shape +tree.num <- recipe$recipe_number +fileout <- recipe$fileout # card name +if (!is.null(fileout)) { + fileout <- paste0(dirname(fileout), "/tree_", tree.num, '.png') +} + + +# Sanity checks +if (length(title.col) != 1) stop("Title can only have one color.") +if (length(tree.col) != 1) stop("Tree leaves can only have one color.") +if (length(trunk.col) != 1) stop("Tree trunk can only have one color.") +if (length(bg.col) != 1) stop("Background can only have one color.") +if (length(star) != 1) stop("Star can only have one shape.") +#if (!star %in% c('star', 'heart', 'poop', 'smile', 'smirk')) stop("Star can only be star, heart, poop, smile, or smirk.") + +if (!is.null(fileout)) { + png(fileout, width = 480, height = 480) +} else { + dev.new(width = 6.5, height = 6.5) +} + +ll <- 252 + +par(bg = bg.col) +plot(0, xlim = c(-0.75, 0.75), axes = F, xlab = "", ylab = "", ylim = c(-50, ll + 48)) + +# trunk +rect(-0.1, -50, 0.08, 1, col = trunk.col, border = trunk.col) + +# star glow +if (star != 'heart') { + col_star <- rgb(1, 1, 0, 0.05) +} else { + col_star <- rgb(1, 0.6, 0.8, 0.1) +} +for(i in 12:1) { + points(-0.02, ll + 1, pch = 19, col = col_star, cex = 5 + (0.65 * i)) +} + +# Tree +s <- (ll:1)/1000 +a <- 0 +i_seq <- rep(exp(-seq(1, 125, by = 2) * 0.016), 4) * s + s + +if (tree.col == 'green') { + for (i in i_seq) { + a <- a + 1 + j_seq <- seq(-i, i, 0.1) + for (j in j_seq) { + points(j, a, col = rgb(0, i * 0.7 + 0.2 * runif(1, 0.6, 1) - 0.5 * j^2, 0), + pch = 8, cex = 2.8, lwd = 1.5) + } + } +} else if (tree.col == 'blue') { + for (i in i_seq) { + a <- a + 1 + j_seq <- seq(-i, i, 0.1) + for (j in j_seq) { + points(j, a, col = rgb(0, 0, i * 0.7 + 0.2 * runif(1, 0.6, 1) - 0.5 * j^2), + pch = 8, cex = 2.8, lwd = 1.5) + } + } +} else if (tree.col == 'red') { + for (i in i_seq) { + a <- a + 1 + j_seq <- seq(-i, i, 0.1) + for (j in j_seq) { + points(j, a, col = rgb(i * 0.7 + 0.2 * runif(1, 0.6, 1) - 0.5 * j^2, 0, 0), + pch = 8, cex = 2.8, lwd = 1.5) + } + } +} else if (tree.col == 'yellow') { + for (i in i_seq) { + a <- a + 1 + j_seq <- seq(-i, i, 0.1) + for (j in j_seq) { + points(j, a, col = rgb(i * 0.7 + 0.2 * runif(1, 0.6, 1) - 0.5 * j^2, i * 0.7 + 0.2 * runif(1, 0.6, 1) - 0.5 * j^2, 0), + pch = 8, cex = 2.8, lwd = 1.5) + } + } +} else if (tree.col %in% c('gray', 'grey')) { + for (i in i_seq) { + a <- a + 1 + j_seq <- seq(-i, i, 0.1) + for (j in j_seq) { + points(j, a, col = rgb(i * 0.7 + 0.2 * runif(1, 0.6, 1) - 0.5 * j^2, i * 0.7 + 0.2 * runif(1, 0.6, 1) - 0.5 * j^2, i * 0.7 + 0.2 * runif(1, 0.6, 1) - 0.5 * j^2), + pch = 8, cex = 2.8, lwd = 1.5) + } + } +} else { + stop("Tree leaf color is not supported.") +} + +# Christmas bulbs +cols1 <- sample(c(rgb(1, 0, 0, 0.1), rgb(0, 1, 0, 0.1), rgb(1, 1, 0, 0.1), rgb(0, 0.2, 1, 0.1))) +cols2 <- sample(c(rgb(1, 0, 0, 0.1), rgb(0, 1, 0, 0.1), rgb(1, 1, 0, 0.1), rgb(0, 0.2, 1, 0.1))) +cols3 <- sample(c(rgb(1, 0, 0, 0.1), rgb(0, 1, 0, 0.1), rgb(1, 1, 0, 0.1), rgb(0, 0.2, 1, 0.1))) +cols4 <- sample(c(rgb(1, 0, 0, 0.1), rgb(0, 1, 0, 0.1), rgb(1, 1, 0, 0.1), rgb(0, 0.2, 1, 0.1))) + +## small bulbs +for (i in 11:1) { + points(seq(-0.5, 0.5, 0.1), rep(0, 11), pch = 19, cex = 0.15 + (0.12 * i), col = cols1) + points(seq(-0.375, 0.375, 0.1), rep(64.5, 8), pch = 19, cex = 0.15 + (0.12 * i), col = cols2) + points(seq(-0.25, 0.25, 0.1), rep(125, 6), pch = 19, cex = 0.15 + (0.12 * i), col = cols3) + points(seq(-0.125, 0.125, 0.1), rep(189.5, 3), pch = 19, cex = 0.15 + (0.12 * i), col = cols4) +} + +## balls +loc <- c(runif(8, -0.45, 0.45), runif(8, 0, 30)) +points(loc[1:8], loc[9:16], pch = 19, cex = round(runif(8, 1.8, 2.2), digits = 2), + col = sample(ball.col, 8, replace = TRUE)) +loc <- c(runif(6, -0.25, 0.25), runif(6, 65, 100)) +points(loc[1:6], loc[7:12], pch = 19, cex = round(runif(6, 1.8, 2.2), digits = 2), + col = sample(ball.col, 6, replace = TRUE)) +loc <- c(runif(4, -0.15, 0.15), runif(4, 125, 170)) +points(loc[1:4], loc[5:8], pch = 19, cex = round(runif(4, 1.8, 2.2), digits = 2), + col = sample(ball.col, 4, replace = TRUE)) +points(-0.05, 200, pch = 19, cex = 2, col = sample(ball.col, 1)) +# first layer edge +points(sample(c(-0.45, 0.45), 1), 6, pch = 19, cex = 2, col = sample(ball.col, 1)) +# second layer edge +points(sample(c(-0.32, 0.32), 1), 66, pch = 19, cex = 2, col = sample(ball.col, 1)) +# third layer edge +points(sample(c(-0.2, 0.2), 1), 130, pch = 19, cex = 2, col = sample(ball.col, 1)) + +# Star +if (star == 'star') { + points(-0.02, 251, pch = 19, cex = 4.3, col = 7) + points(-0.02, 254, pch = emo::ji(keyword = 'star'), cex = 8, col = 7) +} else if (star == 'heart') { + points(-0.02, 251, pch = emo::ji(keyword = 'heart'), cex = 5, col = 'hotpink') +} else if (star == 'poop') { + points(-0.02, 253, pch = 19, cex = 5, col = 'black') + points(-0.02, 256, pch = emo::ji(keyword = 'poop'), cex = 6, col = 'chocolate4') +} else if (star == 'smile') { + points(-0.02, 253, pch = 19, cex = 7.2, col = rgb(1, 1, 0, alpha = 0.95)) + points(-0.02, 253, pch = emo::ji(keyword = 'smile'), cex = 4, col = rgb(0, 0, 0, alpha = 0.8)) +} else if (star == 'smirk') { + points(-0.02, 253, pch = 19, cex = 7.2, col = rgb(1, 1, 0, alpha = 0.95)) + points(-0.02, 253, pch = emo::ji(keyword = 'smirk'), cex = 4, col = rgb(0, 0, 0, alpha = 0.8)) +} else { + points(-0.02, 253, pch = 19, cex = 7.2, col = rgb(1, 1, 0, alpha = 0.95)) + points(-0.02, 253, pch = emo::ji(keyword = star), cex = 4, col = rgb(0, 0, 0, alpha = 0.8)) +} + +# Snowflakes +if (snow.level == 'none') { + snow_num <- c(0, 0) +} else if (snow.level == 'below_normal') { + snow_num <- c(40, 60) +} else if (snow.level == 'normal') { + snow_num <- c(80, 100) +} else if (snow.level == 'above_normal') { + snow_num <- c(160, 200) +} +points(runif(snow_num[1], -1, 1), runif(snow_num[1], -50, ll + 60), + col = sample(snow.col, snow_num[1], replace = TRUE), + cex = sample(seq(0.6, 1.5, length.out = snow_num[1])), + pch = emo::ji(keyword = 'snowflake')) +points(runif(snow_num[2], -1, 1), runif(snow_num[2], -50, -45), + col = sample(snow.col, snow_num[2], replace = TRUE), + cex = sample(seq(0.6, 1.5, length.out = snow_num[2])), + pch = emo::ji(keyword = 'snowflake')) + + # Add author + mtext(paste0(author, ' - ', title), side = 1, line = 1, col = title.col) + + if (!is.null(fileout)) { + dev.off() + } +} diff --git a/Christmas/2023/ParallelChristmas.sh b/Christmas/2023/ParallelChristmas.sh new file mode 100755 index 0000000000000000000000000000000000000000..82953a00790369b60abb4eacfb6adb3463cecf0b --- /dev/null +++ b/Christmas/2023/ParallelChristmas.sh @@ -0,0 +1,90 @@ +#!/bin/bash + +################################################################################ +## Launcher script for Christmas Trees! +################################################################################ +## +## It reads a Tree recipe recipe and splits it into atomic recipes. +## Then each atomic recipe is launched as an independent job with SLURM. +## +## Merry Christmas! +## +################################################################################ + +# Usage statement + +function usage +{ + echo "Usage: $0 " + echo " " + echo " : Path to the Christmas Tree recipe." + echo " " +} + +if [[ ( $@ == "--help") || $@ == "-h" ]]; then + usage + exit 0 +fi + +# Assign arguments +recipe=$1 + +umask 002 + +# Check recipe +if [ ! -f "$recipe" ]; then + echo "Could not find the recipe file: $recipe" + usage + exit 1 +fi + +# Define tmp file to store necessary information +tmpfile=$(mktemp ${TMPDIR-/tmp}/XMAS.XXXXXX) + +# Divide recipes and get layout +module load R/4.1.2-foss-2019b +echo "Counting the number of Christmas trees..." +Rscript read_ChristmasRecipe.R ${recipe} ${tmpfile} +nrows=$( head -1 $tmpfile | tail -1 ) +ncols=$( head -2 $tmpfile | tail -1 ) +outdir=$( head -3 $tmpfile | tail -1 ) +outfile=$( head -4 $tmpfile | tail -1 ) + +# Create directory for slurm output +logdir=${outdir}/slurm_logs/ +mkdir -p $logdir +echo "Christmas tree logs will be stored in $logdir" + +# Launch one job per atomic recipe +job_number=0 +# Create empty array to store all the job IDs for the verification jobs +tree_job_list=() +echo "Sending tree requests to Santa's workshop..." + +# Loop over atomic recipes +for atomic_recipe in ${outdir}/santas_workshop/atomic_recipe_??.yml; do + job_number=$(($job_number + 1)) + job_name=$(basename $outdir)_$(printf %02d $job_number) + outlog=${logdir}/create_tree-${job_name}.out + errlog=${logdir}/create_tree-${job_name}.err + # Send batch job and capture job ID + job_ID=$(sbatch --parsable --output=$outlog --error=$errlog --time=00:10:00 plot_ChristmasTree.sh ${atomic_recipe}) + # Add job ID to array + tree_job_list+=($job_ID) + echo "Submitted tree request $job_ID" +done + +# Submit layout job with dependency on atomic tree jobs, passed as a +# comma-separated string. The layout job will not run until all the +# verification jobs have finished successfully. +# If any of the jobs fail, it will be canceled. +echo "Elves will build the Christmas card when all the trees are done..." +outlog=${logdir}/create_card.out +errlog=${logdir}/create_card.err +card_ID=$(sbatch --parsable --dependency=afterok:$(IFS=,; echo "${tree_job_list[*]}") --output=$outlog --error=$errlog --time=00:10:00 create_layout.sh $nrows $ncols $outfile) +echo "Submitted card request $cardID" + +# Clean temporary files +rm $tmpfile +# rm ${outdir}/santas_workshop/* +umask 022 diff --git a/Christmas/2023/README.md b/Christmas/2023/README.md new file mode 100644 index 0000000000000000000000000000000000000000..b6ad3b22a52e1671dca8fe896234a25c328acfea --- /dev/null +++ b/Christmas/2023/README.md @@ -0,0 +1,31 @@ +--- +title: Christmas Tree 2023 +author: Victòria Agudetse and An-Chi Ho +date: Christmas Season 2023 +--- + +Following the last Christmas activity (check it [here](Christmas/2022)), we're going to do it again, but, in parallel! + +We can put multiple options in the recipe, and several atomic recipes will be created. Then, submit jobs on Nord3, one job for one atomic recipe. In the end, use `montage` to combine all the plots together into one Christmas card. + +**[Steps]** + +1. Get all the code and recipe from Rsenal GitLab repo https://earth.bsc.es/gitlab/es/rsenal/-/tree/main/Christmas/2023/. Put them somewhere in Nord3, e.g., `/esarchive/scratch/`. +2. Modify the recipe. +3. ssh to Nord3v2, move to the directory that you saved the code. +4. Run this command: `bash ParallelChristmas.sh .yml` +5. A PNG file will be generated where you specify in the recipe `$fileout`. You can also find the logs in `slum_logs/` and atomic recipes and plots under `santas_workshop/`, at the same level of `$fileout`. +6. Share your Christmas card with us! + + +**[Notes]** + +* emoji (i.e., `$star` in the recipe) may or may not show properly depending on your environment. Currently on Nord3, the following ones can be shown: + +> heart, smile, smirk, sun, sparkle, angry, sad, cat, crush, cry, blush, sweat, disappointed, grin, frown, haha, ill, meh, wink, oops, green, prank, relieved, silly, wow, victory,radioactive, biohazard, warning,yin_yang, aries, taurus, gemini, cancer, leo, virgo, libra, scorpius, sagittarius, capricorn, aquarius, pisces, medical_symbol, ... + +* If the plots are not complete, check logs to see if the recipe has wrong items. + +* Remember to clean `santas_workshop/` before having a new run, so the script won't take the old plots into the card. + + diff --git a/Christmas/2023/create_layout.sh b/Christmas/2023/create_layout.sh new file mode 100755 index 0000000000000000000000000000000000000000..1d0dbf7678e6dfb903366d74b7758fa39bd63f71 --- /dev/null +++ b/Christmas/2023/create_layout.sh @@ -0,0 +1,21 @@ +#!/bin/bash + +#SBATCH -J XMAS_CARD +#SBATCH --kill-on-invalid-dep=yes + +# Slurm directive description: +# -J: job name +# --kill-on-invalid-dep: Whether to kill the job if the dependencies are +# not satisfied. If the verification jobs fail, the scorecards job +# will be canceled. + +set -vx + +nrows=$1 +ncols=$2 +outfile=$3 + +outdir="$(dirname "${outfile}")" + +# Create image layout +montage ${outdir}/santas_workshop/tree_??.png -tile ${ncols}x${nrows} -geometry +1+1 ${outfile} diff --git a/Christmas/2023/decide_nice_layout.R b/Christmas/2023/decide_nice_layout.R new file mode 100755 index 0000000000000000000000000000000000000000..703dd3ff17dfb4778728e1918ece9428978cda63 --- /dev/null +++ b/Christmas/2023/decide_nice_layout.R @@ -0,0 +1,20 @@ +decide_nice_layout <- function(num_recipe) { + + if (num_recipe == 1) { + mfrow <- c(1, 1) + } else if (num_recipe == 2) { + mfrow <- c(2, 1) + } else if (num_recipe %in% 3:4) { + mfrow <- c(2, 2) + } else if (num_recipe %in% 5:6) { + mfrow <- c(3, 2) + } else if (num_recipe %in% 11:12) { + mfrow <- c(3, 4) + } else if (num_recipe %in% 17:24) { + mfrow <- c(4, 6) + } else { + mfrow <- rep(ceiling(sqrt(num_recipe)), 2) + } + return(mfrow) +} + diff --git a/Christmas/2023/divide_recipe.R b/Christmas/2023/divide_recipe.R new file mode 100755 index 0000000000000000000000000000000000000000..48c20fe1d0ee3f8bc621f914c8791ace49562e4a --- /dev/null +++ b/Christmas/2023/divide_recipe.R @@ -0,0 +1,87 @@ +divide_recipe <- function(recipe) { + + beta_recipe <- list(Description = recipe$Description, + background = NULL, + tree = NULL, + balls = NULL, + snow = NULL, + star = NULL, + fileout = paste0(dirname(recipe$fileout), + "/santas_workshop/", + basename(recipe$fileout))) + + all_recipes <- rep(list(beta_recipe), length(recipe$background)) + for (col in 1:length(recipe$background)) { + all_recipes[[col]]$background <- recipe$background[[col]] + } + + for (col in 1:length(recipe$tree)) { + for (reci in 1:length(all_recipes)) { + all_recipes[[reci]]$tree <- recipe$tree[[col]] + } + if (col == 1) { + recipes <- all_recipes + } else { + recipes <- append(recipes, all_recipes) + } + } + all_recipes <- recipes + + recipes <- list() + for (col in 1:length(recipe$balls)) { + for (reci in 1:length(all_recipes)) { + all_recipes[[reci]]$balls <- recipe$balls[[col]] + } + if (col == 1) { + recipes <- all_recipes + } else { + recipes <- append(recipes, all_recipes) + } + } + all_recipes <- recipes + + recipes <- list() + for (col in 1:length(recipe$snow)) { + for (reci in 1:length(all_recipes)) { + all_recipes[[reci]]$snow <- recipe$snow[[col]] + } + if (col == 1) { + recipes <- all_recipes + } else { + recipes <- append(recipes, all_recipes) + } + } + all_recipes <- recipes + + + recipes <- list() + for (col in 1:length(recipe$star)) { + for (reci in 1:length(all_recipes)) { + all_recipes[[reci]]$star <- recipe$star[[col]] + } + if (col == 1) { + recipes <- all_recipes + } else { + recipes <- append(recipes, all_recipes) + } + } + all_recipes <- recipes + + # Save all recipes in separate YAML files + for (reci in 1:length(all_recipes)) { + if (reci < 10) { + recipe_number <- paste0("0", reci) + } else { + recipe_number <- reci + } + all_recipes[[reci]]$recipe_number <- recipe_number + if (!dir.exists(paste0(dirname(recipe$fileout), "/santas_workshop/"))) { + dir.create(paste0(dirname(recipe$fileout), "/santas_workshop/"), + recursive = TRUE) + } + write_yaml(all_recipes[[reci]], + paste0(dirname(recipe$fileout), "/santas_workshop/atomic_recipe_", + recipe_number, ".yml")) + } + return(list(n_atomic_recipes = length(all_recipes))) +} diff --git a/Christmas/2023/myXmasCard2023.png b/Christmas/2023/myXmasCard2023.png new file mode 100644 index 0000000000000000000000000000000000000000..f622900a15928f676c316e2acbde49ba1e7f697a Binary files /dev/null and b/Christmas/2023/myXmasCard2023.png differ diff --git a/Christmas/2023/plot_ChristmasTree.R b/Christmas/2023/plot_ChristmasTree.R new file mode 100755 index 0000000000000000000000000000000000000000..258bd3d3fa9058ff77920b589f0cc7854ab91346 --- /dev/null +++ b/Christmas/2023/plot_ChristmasTree.R @@ -0,0 +1,7 @@ +library(yaml) +source('ChristmasTree.R') +args <- commandArgs(trailingOnly = TRUE) +recipe <- read_yaml(args[1]) +options(bitmapType='cairo') +ChristmasTree(recipe) + diff --git a/Christmas/2023/plot_ChristmasTree.sh b/Christmas/2023/plot_ChristmasTree.sh new file mode 100755 index 0000000000000000000000000000000000000000..d0a9cc14194994fbf1dc844ae14e715ef5c32706 --- /dev/null +++ b/Christmas/2023/plot_ChristmasTree.sh @@ -0,0 +1,14 @@ +#!/bin/bash + +#SBATCH -J XMAS_TREE + +# Slurm directive description: +# -J: job name + +set -vx + +atomic_recipe=$1 + +module load R/4.1.2-foss-2019b + +Rscript plot_ChristmasTree.R ${atomic_recipe} diff --git a/Christmas/2023/read_ChristmasRecipe.R b/Christmas/2023/read_ChristmasRecipe.R new file mode 100755 index 0000000000000000000000000000000000000000..d0cfd9615ea7dd5b237d25c267e432e57457795d --- /dev/null +++ b/Christmas/2023/read_ChristmasRecipe.R @@ -0,0 +1,18 @@ +arguments <- commandArgs(trailingOnly = TRUE) +names(arguments) <- c("recipe", "tmpfile") + +library(yaml) +source('ChristmasTree.R') +source('divide_recipe.R') +source('decide_nice_layout.R') +recipe <- read_yaml(arguments[1]) +atomic_recipes <- divide_recipe(recipe) + +## TODO: Export necessary variables +layout <- decide_nice_layout(atomic_recipes$n_atomic_recipes) +sink(arguments[2], append = FALSE) +cat(paste0(layout[1]), "\n") +cat(paste0(layout[2]), "\n") +cat(paste0(dirname(recipe$fileout), "\n")) +cat(recipe$fileout) +sink() diff --git a/Christmas/2023/recipe_ChristmasTree_template.yml b/Christmas/2023/recipe_ChristmasTree_template.yml new file mode 100755 index 0000000000000000000000000000000000000000..dcbd706d756b18582e2ac4141c4161c99720d6a3 --- /dev/null +++ b/Christmas/2023/recipe_ChristmasTree_template.yml @@ -0,0 +1,30 @@ +Description: + Title: + author: BSCES # Your name + text: All I want for Christmas is... # A merry message + color: 'white' # One random color for text +background: + - {color: lightblue} # One random color, e.g., 'lightblue', '#612794' + - {color: navy} + - {color: gold} +tree: +# leaves: One of the following: 'green', 'blue', 'red', 'yellow', 'grey' +# trunk: One random color, e.g., 'darkgoldenrod4', '#5829C4' + - {leaves: green, trunk: darkgoldenrod4} + - {leaves: blue, trunk: grey} +balls: +# Multiple random colors + - {color: oldlace lightcyan lightgoldenrod1} + - {color: lightcyan gold pink} +snow: +# level: 'none', 'below_normal', 'normal', 'above_normal' +# color: Multiple random colors + - {level: below_normal, color: ghostwhite lightsteelblue1} +star: +# Emoji name. Suggestions: 'heart', 'smile', 'smirk', 'blush', 'prank' + - {shape: heart} + - {shape: blush} + - {shape: smile} +# A file path under /esarchive/scratch/ +fileout: '/esarchive/scratch//myXmasCard2023.png' +