Julia Reference

Julia Documentation - https://docs.julialang.org/en/v1/
Julia by Examples - https://juliabyexample.helpmanual.io/
The fast track to Julia - https://juliadocs.github.io/Julia-Cheat-Sheet/
Julia Plots - https://docs.juliaplots.org/
Julia Community (Standards) - https://julialang.org/community/standards/
Julia Data Science - https://juliadatascience.io/
Plotting with Makie - https://docs.makie.org/stable/
Numerical Computing in Julia -
https://www.matecdev.com/posts/julia-tutorial-science-engineering.html
Julia interface to Gnuplot - https://gcalderone.github.io/Gnuplot.jl/
Gaston Julia package for plotting - https://mbaz.github.io/Gaston.jl/stable/
Introduction to Julia's Debugger -
https://webpages.csus.edu/fitzgerald/julia-debugger-tutorial/
Julia Mathematics - https://docs.julialang.org/en/v1/base/math/#Base.ceil
A Deep Introduction to Julia for Data Science and Scientific Computing -
http://ucidatascienceinitiative.github.io/IntroToJulia/
Julia SOS - https://www.juliasos.com/
Profile Julia code - https://opensourc.es/blog/constraint-solver-profiling/
Parallel processing in Julia - https://berkeley-scf.github.io/tutorial-parallelization/parallel-julia

# Julia-1.9.2 Current Stable version installation
$ wget julia-1.9.2-linux-x86_64.tar.gz
$ tar -xzvf julia-1.9.2-linux-x86_64.tar.gz
$ mv julia-1.9.2/ ~/.julia/
$ set path in ~/.bashrc [export PATH="$PATH:/home/saran/.julia/bin"]
$ source .bashrc

# upgrade julia-1.9.2 to julia-1.9.3 [https://julialang.org/downloads/]
1. download current stable release: v1.9.3 [Generic Linux on x86 - 64-bit (glibc)]
$ wget https://julialang-s3.julialang.org/bin/linux/x64/1.9/julia-1.9.3-linux-x86_64.tar.gz
2. untar julia-1.9.3-linux-x86_64.tar.gz
$ tar -xzvf julia-1.9.3-linux-x86_64.tar.gz
3. move julia-1.9.3/ to ~/.julia-1.9.3/
$ mv downloads/julia-1.9.3/ .julia-1.9.3/
4. move directories compiled/ environments/ logs/ packages/ registries/ scratchspaces/
$ mv ~/.julia-1.9.2/compiled/ ~/.julia-1.9.3/
$ mv ~/.julia-1.9.2/environments/ ~/.julia-1.9.3/
$ mv ~/.julia-1.9.2/logs/ ~/.julia-1.9.3/
$ mv ~/.julia-1.9.2/packages/ ~/.julia-1.9.3/
$ mv ~/.julia-1.9.2/registries/ ~/.julia-1.9.3/
$ mv ~/.julia-1.9.2/scratchspaces/ ~/.julia-1.9.3/
5. remove ~/.julia-1.9.2
$ rm -rf .julia/
6. rename ~/.julia-1.9.3 to ~/.julia/
$ mv ~/.julia-1.9.3/ ~/.julia/
7. activate virtual environment, check status and update
$ source .venv/dsci/bin/activate
(@v1.9) pkg> status
(@v1.9) pkg> update

# install Julia
Install the latest Julia version (v1.10.4 June 4, 2024) by running this in your
terminal: curl -fsSL https://install.julialang.org | sh

Once installed julia will be available via the command line interface.

This will install the Juliaup installation manager, which will automatically install
julia and help keep it up to date. The command juliaup is also installed. To install
different julia versions see juliaup --help.

# Types - https://docs.julialang.org/en/v1/manual/types/
# Arrays - https://docs.julialang.org/en/v1/base/arrays/
# Mathematical operations and Elementary functions -
    https://docs.julialang.org/en/v1/mathematical-operations/
# concise tutorial - https://syli.gitbook.io/julia-language-a-concise-tutorial/
# random num - www.juliabloggers.com/basics-of-generating-random-numbers-in-julia/

$ julia
> versioninfo()
> using Pkg
> Pkg.add("package_name")

# update julia to latest stable version
(@v1.8) pkg> add UpdateJulia
julia> using UpdateJulia
julia> update_julia()
Ref: https://stackoverflow.com/questions/30555225/how-to-upgrade-julia-to-a-new-release

# remove broken link if already present
cd .local/bin/
rm julia
rm julia-1.8
rm julia-1.8.2

# create symbolic link
ln -s /home/saran/.julia/bin/julia /home/saran/.local/bin/julia
ln -s /home/saran/.julia/bin/julia /home/saran/.local/bin/julia-1.8
ln -s /home/saran/.julia/bin/julia /home/saran/.local/bin/julia-1.8.2

# in the Julia REPL, hit ] to enter package mode and then enter
(@v1.8) pkg> help
(@v1.8) pkg> status
(@v1.8) pkg> status --outdated
(@v1.8) pkg> update
(@v1.8) pkg> add <package>
(@v1.8) pkg> rm <package>

# the julia internal variable Sys.WORD_SIZE indicates
# whether the target system is 32-bit or 64-bit
Sys.WORD_SIZE

# to quit
exit()

# Source Installation
sudo apt-get install libncurses-dev libncurses5-dev libtinfo-dev
git clone git://github.com/JuliaLang/julia.git
cd julia
make
... go take a long nap ...
git pull && make (to upgrade Julia)

# virtual environment
pkg> add VirtualEnv
$ mkdir .venv
$ venv -h                           # display help
$ venv --prompt dsci .venv/dsci/    # creating virtual environment with prompt name (dsci)
$ source .venv/dsci/bin/activate    # activating virtual environment
$ deactivate                        # deactivating environment

note: if above method of creating virtual environment not work then
pkg> add Comonicon
pkg> add VirtualEnv
$ mkdir .venv
$ julia -e 'using VirtualEnv; venv("/home/user/.venv/dsci")'
$ source .venv/dsci/bin/activate
$ deactivate
Reference: Documentataion - https://mehalter.github.io/VirtualEnv.jl/stable/

# creating your own environments
(@v1.8) pkg> activate fdtd
Activating new environment at '~/fdtd/Project.toml'

(fdtd) pkg> st
    Status '~/fdtd/Project.toml' (empty project)

Note that the REPL prompt changes when the new project is activated. Until a
package is added, there are no files in this environment and the directory to
the environment might not even be created:

julia> isdir("fdtd")
false

(fdtd) pkg> ad PyPlot

julia> isdir("fdtd")
true

julia> readdir("fdtd")

julia> print(read(joinpath("fdtd", "Project.toml"), String))

julia> print(read(joinpath("fdtd", "Manifest.toml"), String))

# using someone else's project
Simply clone their project using e.g. git clone, cd to the project directory and
call

shell> git clone https://github.com/julialang/example.jl.git
Cloning into 'example.jl'...
...

(@v1.8) pkg> activate example.jl
Activating project at '~/example.jl'

(example) pkg> instantiate
    No Changes to '~/example.jl/Project.toml'
    No Changes to '~/example.jl/Manifest.toml'

Reference: https://pkgdocs.julialang.org/v1/environments/

# load a file into the julia console
> include('program.jl')

# time function
@time function(x)

# import benchmark library
using BenchmarkTools

# benchmark function
@benchmark function(x)

# benchmark function with time tolerance
@benchmark function() seconds=1 time_tolerance=0.01
@benchmark function(x) seconds=1 time_tolerance=0.01

# print the minimum time and memory allocation
@btime function(x)

# print minimum time in seconds
@belapsed function(x)

# import profiling library
using Profile

# profile function
@profile function(x)

# print the results
Profile.print()

# clear the profile results
Profile.clear()

# numerical type hierarchy

Number
|
|----- Complex
|----- Real
|----- AbstractFloat
|      |_____ BigFloat
|      |_____ Float16
|      |_____ Float32
|      |_____ Float64
|
|----- AbstractIrrational
|      |_____ Irrational
|
|----- Integer
|      |
|      |----- Bool
|      |
|      |----- Signed
|      |      |_____ BigInt
|      |      |_____ Int128
|      |      |_____ Int16
|      |      |_____ Int32
|      |      |_____ Int64
|      |      |_____ Int8
|      |
|      |_____ Unsigned
|             |_____ UInt128
|             |_____ UInt16
|             |_____ UInt32
|             |_____ UInt64
|             |_____ UInt8
|
|_____ Rational


# arithmetic operators
Expression   Name               Description
+x           unary plus         the identity operation
-x           unary minus        maps values to their additive inverses
x + y        binary plus        performs addition
x - y        binary minus       performs subtraction
x * y        times              performs multiplication
x / y        divide             performs division
x \div y     integer divide     x / y, truncated to an integer
x \ y        inverse divide     equivalent to y / x
x ^ y        power              raises x to the yth power
x % y        remainder          equivalent to rem(x, y)

# boolean operators
Expression      Name
!x              negation
x && y          short-circuiting and
x || y          short-circuiting or

# bitwise operators
Expression      Name
~x              bitwise not
x & y           bitwise and
x | y           bitwise or
xor(x, y)       bitwise xor (exclusive or)
nand(x, y)      bitwise nand (not and)
nor(x, y)       bitwise nor (not or)
x >>> y         logical shift right
x >> y          arithmetic shift right
x << y          logical/arithmetic shift left

# updating operators
The updating versions of all the binary arithmetic and bitwise operators are:
+=  -=  *=  /=  \=  \div=   %=  ^=  &=  |=  >>>=    >>=     <<=

# numeric comparisons
Operator        Name
==              equality
!=              inequality
<               less than
<=              less than or equal to
>               greater than
>=              greater than or equal to

Function        Tests if
isequal(x, y)   x and y are identical
isfinite(x)     x is a finite number
isinf(x)        x is infinite
isnan(x)        x is not a number

# rounding functions
Function        Description                         Return type
round(x)        round x to the nearest integer      typeof(x)
round(T, x)     round x to the nearest integer      T
floor(x)        round x towards -Inf                typeof(x)
floor(T, x)     round x towards -Inf                T
ceil(x)         round x towards +Inf                typeof(x)
ceil(T, x)      round x towards +Inf                T
trunc(x)        round x towards zero                typeof(x)
trunc(T, x)     round x towards zero                T

# division functions
Function        Description
div(x, y)       truncated division; quotient rounded towards zero
fld(x, y)       floored division; quotient rounded towards -Inf
cld(x, y)       ceiling division; quotient rounded towards +Inf
rem(x, y)       remainder; satisfies x == div(x,y)*y + rem(x,y); sign matches x
mod(x, y)       modulus; satisfies x == fld(x,y)*y + mod(x,y); sign matches y
mod1(x, y)      mod with offset 1; returns x(0, y] for y>0 or x[y,0) for y<0
mod2pi(x)       modulus with respect to 2pi; 0 <= mod2pi(x) < 2pi
divrem(x, y)    returns (div(x, y), rem(x, y))
fldmod(x, y)    returns (fld(x, y), mod(x, y))
gcd(x, y...)    greatest positive common divisor of x, y, ...
lcm(x, y...)    least positive common multiple of x, y, ...

# sign and absolute value functions
Function        Description
abs(x)          a positive value with the magnitude of x
abs2(x)         the squared magnitude of x
sign(x)         indicates the sign of x, returning -1, 0, or +1
signbit(x)      indicates whether the sign bit is on (true) or off(false)
copysign(x, y)  a value with the magnitude of x and the sign of y
flipsign(x, y)  a value with the magnitude of x and the sign of x*y

# powers, logs and roots
Function        Description
sqrt(x)         square root of x
cbrt(x)         cube root of x
hypot(x, y)     hypotenuse of right-angled triangle with other sides of length x and y
exp(x)          natural exponential function at x
expm1(x)        accurate exp(x)-1 for x near zero
ldexp(x, n)     x*2^n computed efficiently for integer values of n
log(x)          natural logarithm of x
log(b, x)       base b logarithm of x
log2(x)         base 2 logarithm of x
log10(x)        base 10 logarithm of x
log1p(x)        accurate log(1+x) for x near zero
exponent(x)     binary exponent of x
significand(x)  binary significand (a.k.a. mantissa) of a floating-point number x

# trigonometric and hyperbolic functions
sin     cos     tan     cot     sec     csc
sinh    cosh    tanh    coth    sech    csch
asin    acos    atan    acot    asec    acsc
asinh   acosh   atanh   acoth   asech   acsch
sinc    cosc

sinpi(x) == sin(pi*x)
cospi(x) == cos(pi*x)

In order to compute trigonometric functions with degrees instead of radians, suffix
the function with d. For example, sind(x) computes the sine of x where x is specified
in degrees. The complete list of trigonometric functions with degree variants is:

sind    cosd    tand    cotd    secd    cscd
asind   acosd   atand   acotd   asecd   acscd

Reference: https://docs.julialang.org/en/v1/manual/mathematical-operations/

# conversion of Integer to Float
Float64(2)          # 2.0   (double-precision)
Float32(2)          # 2.0f0 (single-precision)
Float16(2)          # Float16(2.0) (half-precision)

# conversion of Float to Integer (the float must coincide with an integer)
Int64(2.0)          # 2
Int64(2.4)          # Error!
floor(Int64, 2.4)   # 2
ceil(Int64, 2.4)    # 3
round(Int64, 2.4)   # 2

# math constants
\pi + Tab
pi
\euler + Tab
MathConstants.e

# Integer division
By default, dividing two integers could return a float. If we are interested in
the integer (or Euclidean) division, the div function (or \div, unicode symbol)
returns the quotient, while for the remainder we have the rem function, or %.

a = 1/2             # 0.5 (Float64)

div(x, y) computes x/y, truncated to an integer
div(10, 3)          # 3
\div(10, 3)         # 3

fld(x, y) computes x/y, truncated to an integer
fld(10, 3)          # 3

Note: prefer fld() than div(), since div() not work for negative integer
div(-3, 2)          # -1 whereas the correct answer is -2
fld(-3, 2)          # -2

rem(x, y) computes the remainder of x/y
rem(10, 3)          # 1
10%3                # 1

# gotcha! Integer vs Float
In Julia, if you input a number like

a = 2

you'll be getting an Int64 by default. This can lead to errors if we are not
careful. For example, consider the following operation

a = 1/2^64
Inf

Now, that's unexpected. Two things just happened. In the first place, an Int64
will overflow past 2^{64}, and the default overflow value is 0.

2^64                # returns 0

In the second place, in Julia 1 divided by 0 returns Inf. We just computed 1/0.

We have some alternatives:
1/2.0^64            # 5.421010862427522e-20
(1/2)^64            # 5.421010862427522e-20
1.0/2^64            # Inf


# file system [Reference: https://docs.julialang.org/en/v1/base/file/]
> pwd()
"/home/user"
> cd("/home/user/Directory")
> pwd()
"/home/user/Directory"
> cd()
> pwd()
"/home/user"
> cd(readdir, "/home/user/Directory")
"file1.sh"
"file2.jl"
> pwd()
"/home/user"

# plots in Julia - https://juliaplots.github.io/

# add package with specific version number
(@v1.6) pkg> add FiniteDiff@1.2

# display the type of a variable
typeof(variable_name)

# useful commands
ctrl + D    # exits Julia
ctrl + C    # interrupts computations
?           # enters help mode
;           # enters system shell mode
]           # enters package manager mode
ctrl + l    # clears screen
putting ; after the expression will disable showing its value in REPL # not needed in script

# essential functions in the Julia REPL (they can be also invoked in scripts)
@edit max(1,2)     # show the definition of max function when invoked with arguments 1 and 2
varinfo()          # list of global variables and their types
cd("/home/user")   # change working directory to /home/user/
pwd()              # get current working directory
include("file.jl") # execute source file
exit(1)            # exit Julia with code 1 (exit code 0 is used by default)
clipboard([1,2])   # copy data to system clipboard
clipboard()        # load data from system clipboard as a string

# configuring PyCall to use a different python version
julia> ENV["PYTHON"]="/home/saran/.envn/dsci/bin/python"
"/home/saran/.envn/dsci/bin/python"
julia> using Pkg
julia> Pkg.bulid("PyCall")

Matplotlib uses matplotlibrc configuration files to customize all kinds of properties.
To display where the currently active matplotlibrc file was loaded from, one can do the
following:
>>> import matplotlib
>>> matplotlib.matplotlib_fname()
'/home/user/.config/matplotlib/matplotlibrc'

Ref: PyPlot.jl - https://juliapackages.com/p/pyplot

# changing font style for latex rendering using PyPlot
import PyPlot
const plt = PyPlot
plt.rc("font", family="serif", weight="normal", size=8)
plt.rc("axes", labelsize=10)
https://discourse.julialang.org/t/changing-font-style-for-latex-rendering-using-PyPlot/5910

# numerical methods for linear algebra
Ref: https://julia.quantecon.org/tools_andtechniques/iterative_methods_sparsity.html
using LinearAlgebra
create 2x3 matrix: A = [2 -4 8.2; -5.5 3.5 63]
size of a matrix as a pair: A_rows, A_cols = size(A)
size of rows of a matrix: A_rows = size(A)[1]
size of columns of a matrix: A_cols = size(A)[2]
condition number of a matrix A: cond(A)
inverse of a matrix A: inv(A)
trace of a matrix A: tr(A)
determinant of a matrix A: det(A)
identity matrix: I = eye(n)
zeros of a matrix: A = zeros(m, n)
ones of a matrix: A = ones(m, n)
transpose of a matrix: A'
+/- are used for matrix addition/subtraction
(matrices must have the same size): [4.0 7; -10.6 89.8] + [19 -34.7; 20 1]
matrix-scalar operations (+,-,*,\) apply elementwise
scalar-matrix multiplication: 10 * [1 2; 3 4] = [1 2; 3 4] * 10
scalar-matrix addition: [10 10; 10 10] + [1 2; 3 4] = [1 2; 3 4] + [10 10; 10 10]
matrix-vector multiplication: [1 2; 3 4] * [5, 6]
matrix-matrix multiplication: [2 4 3; 3 1 5] * [3 10; 4 2; 1 7]
square matrix: A*A == A^2; A*A*A == A^3; A*A*A*A == A^4
sum of entries of a matrix: sum(A)
average of entries of a matrix: mean(A)

# pretty print an array
[Ref: https://stackoverflow.com/questions/62868864/julia-how-to-pretty-print-an-array]
julia> A = zeros(4,4);
julia> show(stdout, A)
[0.0 0.0 0.0 0.0; 0.0 0.0 0.0 0.0; 0.0 0.0 0.0 0.0; 0.0 0.0 0.0 0.0]
julia> show(stdout, "text/plain", A)
4x4 Array{Float64,2}:
 0.0  0.0  0.0  0.0
 0.0  0.0  0.0  0.0
 0.0  0.0  0.0  0.0
 0.0  0.0  0.0  0.0

# dimensions of a matrix
A = zeros(4,4)
size(A)

# matrix operations
A = [1 2 3 4; 5 6 7 8; 9 10 11 12]
typeof(A) # Matrix(Int64) (alias for Array{Int64, 2})

# vector equality
x = [-1.1, 0.0, 3.6, -7.2]
y = copy(x)
y[3] = 4.0
y == x          # false
z = x
z[3] = 4.0
z == x          # true

# printf function in julia
using Printf
@printf("%d \t %d \t %f\n", time, freq, result)

# format program in Julia
Reference: https://domluna.github.io/JuliaFormatter.jl/dev/
julia> using JuliaFormatter
# recursively formats all Julia files in the current directory
julia> format(".")
# formats an individual file
julia> format_file("program.jl")
# formats a string (contents of a Julia file)
julia> format_text(str)

# inner product
x = [1 2 3]
y = [3 2 1]
dot(vec(x), vec(y)) == sum(x.*y)

# https://www.matecdev.com/posts/julia-numerical-integration.html

# functions in Julia
function f(x,y)
    x + y
end

The traditional function declaration syntax demonstrated above is equivalent to
the following compact "assignment form":
f(x,y) = x + y

# version info
julia> versioninfo()
julia> using CUDA
julia> CUDA.versioninfo()
julia> CUDA.memory_status()
julia> CUDA.reclaim()
Ref: https://github.com/JuliaGPU/CUDA.jl/issues/866

# to execute the program inside the julia prompt
julia> include("/path/to/file.jl")

# profiling
https://cuda.juliagpu.org/stable/development/profiling/
https://morioh.com/p/0340fd968665
https://thirld.com/blog/2015/05/30/julia-profiling-cheat-sheet/
https://danmackinlay.name/notebook/julia_debug.html

# parallel programming with Julia using MPI
http://www.claudiobellei.com/2018/09/30/julia-mpi/

# ODE in Julia
https://nextjournal.com/sosiris-de/ode-diffeq

# iteration on each index
N = [10, 20]

for n in eachindex(N) for m in N
    println(n, m)
end
end

Output:
110
120
210
220

can be written as

for n in eachindex(N), m in N
    println(n, m)
end

Output:
110
120
210
220

# append in Julia
str = "institute"
strind = collect(eachindex(str))
print(strind)
[1,2,3,4,5,6,7,8,9]

# enumerate in julia
N = 10
for (m, n) in enumerate(N)
    println(m, n)
end

# symbols
julia> ex = :(a+b*c)
The use of the colon (:) operator in front of (a + b*c) results in the
expression being saved in ex rather than being evaluated. In fact, the
expression would clearly fail unless all the variables had values. The
colon (:) operator creates a symbolic representation, and this is stored
in a data structure Expr and is saved in ex rather than being evaluated.
julia> typeof(ex); # Expr
julia> a = 1.0; b = 2.5; c = 23 + 4im;
julia> eval(ex)
58.5 + 10.0im

# vectorized and devectorized code
# vectorized code
function vecadd1(a,b,c,N)
    for i = 1:N
        c = a + b
    end
end

# devectorized code
function vecadd2(a,b,c,N)
    for i = 1:N, j = 1:length(c)
        c[j] = a[j] + b[j]
    end
end

julia> A = rand(2); B = rand(2); C = zeros(2);
julia> @elapsed vecadd1(A,B,C,10000000)
@elapsed vecadd1(A,B,C,10000000) # 18.418755286

julia> @elapsed vecadd2(A,B,C,10000000)
@elapsed vecadd2(A,B,C,10000000) # 0.524002398

The current situation in Julia is that devectorized code is much quicker than
vectorized.

# get input from the user
print("Enter length of side: ")
s = parse(Int64, readline())
a = s*s
println("\nArea of square is $a")

# floating-point numbers
print("Enter a number: ")
num = parse(Float32, readline())
dnum = parse(Float64, readline())

# get text/string input from the user
print("Enter institute name: ")
name = readline()
println("Institution name is $name")

# escape sequences
\n      newline (line feed)
\$      dollar sign
\\      backslash
\"      double quote
\'      single quote
\r      carriage return
\b      backspace
\t      horizontal tab
\f      form feed (new page)

# reserved words
baremodule  begin   break   catch   const       continue    do          else
elseif      end     export  false   finally     for         function    global
if          import  let     local   macro       module      quote       return
struct      true    try     using   while

# multi-threading: starting Julia with multiple threads
By default, Julia starts up with a single thread of execution. This can be
verified by using the command Threads.nthreads():
julia> Threads.nthreads()
1

The number of execution threads is controlled either by using the -t/--threads
command line argument or by using the JULIA_NUM_THREADS environment variable.

The number of threads can either be specified as an integer (--threads=4) or as
auto (--threads=auto), where auto sets the number of threads to the number of
local CPU threads.

Lets start Julia with 4 threads:
$ julia --threads 4
Lets verify there are 4 threads at our disposal:
julia> Threads.nthreads()
4

But we are currently on the master thread. To check, we use the function
julia> Threads.threadid()
1

# list comprehension
julia> L = ['a', 'b', 'c']
julia> for n in L
        println(n)
    end

julia> x = [n^2 for n in 1:10]
julia> println(x)
[1, 4, 9, 16, 25, 36, 49, 64, 81, 100]

julia> [(x,y) for x in [1,2] for y in [3,1] if x != y]
3-element Vector{Tuple{Int32, Int32}}:
 (1, 3)
 (2, 3)
 (2, 1)

julia> function is_even(a)
            return a%2 == 0
       end

julia> is_even(3)
false

julia> is_even(4)
true

# generators
julia> x = (n^3 for n in 1:10)

julia> for n in x
            println(n)
       end

# functional programming tools
julia> items = [1,2,3,4,5]

julia> function sqr(x)
            return x^2
       end

julia> map(sqr, items)
5-element Vector{Int32}:
  1
  4
  9
 16
 25

julia> sum(items)
15

julia> minimum(items)
1

julia> maximum(items)
5

julia> function f(x)
            return x%2 != 0 && x%3 != 0
       end

julia> filter(f, 2:25)
8-element Vector{Int32}:
  5
  7
 11
 13
 17
 19
 23
 25

# diagm in julia (Ref: Statistics with Julia, Page No: 434)

# information of a package
julia> using SpecialFunctions
julia> ?
help?> SpecialFunctions

# functional
julia> a = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
julia> typeof(a)
Vector{Int32} (alias for Array{Int32, 1})
julia> reduce(+, a)     # 55
julia> sum(a)           # 55
julia> reduce(*, a)     # 3628800
julia> prod(a)          # 3628808
julia> reduce(-, a)     # -53
julia> reduce(/, a)     # 2.7557319223985894e-7

# discard the fractional part
julia> x = 7.3;
julia> n = trunc(x)
7.0
julia> typeof(n)
Float64
julia> n = trunc(Int32, x)
7
julia> typeof(n)
Int32

# alternate function declaration
function f(n)
    n == 0 && return "zero"
    n == 1 && return "ones"
    n == 2 && return "twos"
    return "nothing"
end

julia> f(0)
"zero"

julia> f(1)
"ones"

julia> f(2)
"twos"

julia> f(3)
"nothing'

julia> f(4)
"nothing"

# floating-point representation of 10.375
bitstring(10.375)

# roots and weights of Legendre polynomials
Method 1:
julia> import Polynomials, SpecialPolynomials
julia> p = basis(Legendre, 3)       # legendre polynomial (n = 3)
Legendre(1.0 P_3 (x))
julia> p = convert(Polynomial, p)
Polynomial(-1.5*x + 2.5*x^3)
julia> node_values = roots(p)
3-element Vector{Flo
 -0.7745966692414834
 0.7745966692414834
 0.0
Method 2:
using FastGaussQuadrature
julia> Pn = gausslegendre(3)[1]     # compute roots/nodes of legendre polynomial
3-element Vector{Float64}:          # to compute nodes and weights:
 -0.7745966692414834                # x, w = gausslegendre(3)
 0.0
 0.7745966692414834

# finding type
Reference: https://discourse.julialang.org/t/annotating-types-best-practice-for-beginners/50521/4
similar(x) - creates an uninitialized array with the same type and the same dimensions as x
typeof(x) - returns the type of x, can be used as a constructor
eltype(x) - returns the type of elements in x

# constrain two arguments to be of the same type T
value = add(x::T, y::T) where T = x + y
Now, add(2, 3) returns 5 and add(2, 3.0) returns ERROR: MethodError: `add`
has no method matching add(::Int64, ::Float64) error message.

# package compilation (Precompiling project...)
julia> import Pkg;
julia> Pkg.precompile()

# Gnuplot
julia> using Gnuplot
julia> gpexec("set term sixelgd size 480,360');
julia> @gp hist(randn(1000))

# Gaston and Gnuplot.jl: two philosophies
Gnuplot.jl is another front-end for gnuplot, with comparable capabilities to
Gaston. An example serves to illustrate the differences in how the two packages
approach the interface problem.

Gnuplot.jl:
x = 1:0.1:10
@gp    "set grid" "set key left" "set logscale y"
@gp :- "set title 'Plot title'" "set label 'X label'" "set xrange [0:*]"
@gp :- x x.^0.5 "w l tit 'Pow 0.5' dt 2 lw 2 lc rgb 'red'"
@gp :- x x      "w l tit 'Pow 1'   dt 1 lw 3 lc rgb 'blue'"
@gp :- x x.^2   "w l tit 'Pow 2'   dt 3 lw 2 lc rgb 'purple'"

This shows that Gnuplot.jl essentially allows one to write gnuplot commands
directly in Julia. The same plot in Gaston would be:

x = 1:0.1:10
plot(x, x.^0.5,
    w = "1",
    legend = "'Pow 0.5'",
    dt = 2,
    lw = 2,
    lc = :red,
    Axes(grid = :on,
        key = "left",
        axis = "semilogy"))
plot!(x, x,
    w = :l,
    leg = :Pow_1,
    dt = 1,
    lw = 3,
    lc = :blue)
plot!(x, x.^2,
    curveconf = "w l tit 'Pow 2' dt 3 lw 2 lc 'purple'")

Gaston offers a function-based interface, and gnuplot commands can be specified
in a few different ways, with convenient notation, such as the optional use of
"legend" instead of gnuplot's "title", symbols to avoid typing quote martks(")
all the time.

# help prompt (type ? in julia>)
help?> PyPlot.matplotlib
help?> plt.matplotlib.use
julia> plt.matplotlib.rcParams
julia> Pyplot.matplotlib    # location of matplotlib
help?> plt.rc("classic")

# using classic style
import PyPlot
const plt = PyPlot
plt.matplotlib.style.use("classic")

import PyPlot.matplotlib as plt
plt.style.use("classic")

# factorial
julia> factorial(10)
julia> factorial(big(10))
julia> factorial(big(100))

# access the last element of an list/array
x = [1, 2, 3, 4]
last(x) # 4
x[begin] # 1
x[end] # 4


julia> N = 4
julia> "$N$pi"
julia> "$pi/2"


x1 = LinRange(-pi, pi, 10000) # prefer
x2 = range(-pi, pi, 10000)
julia> @time 2*x1
julia> @time 2*x2
julia> @time x1+x1
julia> @time x2+x2
julia> @timev 2*x1
julia> @timev 2*x2
julia> @timev x1+x1
julia> @timev x2+x2


x = Array{Float64}(undef, 10)  # is equalent to x = zeros(10)

# hit Exc and X for delete notebook cell
# hit Esc and A to create notebook cell above the current cell
# hit Esc and L for line numbers
# Help menu for keyboard shortcuts

Math Teacher's code session 1: (Shift + Enter evaluates the notebook cell)
> isEven(x) = x%2 == 0
> next(x) = isEven(x) ? Int(x/2) : 3x+1
# division always changes to Float64 even if input is Int64
> x = 7
> for _ in 1:20
    print(x, '')
    x = next(x)
  end

> while x != 1
    print(x, '')
    x = next(x)
  end

> function colletz(x)
    while x != 1
        print(x, '')
        x = next(x)
    end
    print(x)
  end

> colletz(7)
> colletz(32)
> collectz(27)

> myArray = [2, 4, 5, 62]

> function colletz(x)
    val = [x]
    while x != 1
        x = next(x)
        push!(val, x)
    end
    return val
  end

> using PyPlot
> yval = colletz(27)
> n = length(yval)
> xval = 1:n
> plot(xval, yval)

> ? push!   # get info of push! function

# Base.Sys
> using Base.Sys
> names(Sys)
> cpu_info()
> cpu_summary()

# concatenating arrays in Julia
1. Using the 'vcat' function
The vcat function in Julia allows to vertically concatenate arrays. This means that the
arrays will be stacked on top of each other to create a new array.
> array1 = [1, 2, 3]
> array2 = [4, 5, 6]
> result = vcat(array1, array2)
In this example, the 'vcat' function is used to concatenate 'array1' and 'array2' into
a new array called 'result'. The resulting array will be '[1, 2, 3, 4, 5, 6]'.

2. Using the 'hcat' function
The hcat function in Julia allows to horizontally concatenate arrays. This means that
the arrays will be placed side by side to create a new array.
> array1 = [1, 2, 3]
> array2 = [4, 5, 6]
> result = hcat(array1, array2)
In this example, the 'hcat' function is used to concatenate 'array1' and 'array2' into
a new array called 'result'. The resulting array will be '[1 4; 2 5; 3 6]'.

3. Using the 'append!' function
The append! function in Julia allows to append one array to another. This modifies the
original array in place.
> array1 = [1, 2, 3]
> array2 = [4, 5, 6]
> append!(array1, array2)
In this example, the 'append!' function is used to append 'array2' to 'array1'. After
the operation, 'array1' will be '[1, 2, 3, 4, 5, 6]'.

If need to create a new array, either vertically or horizontally, then using the 'vcat'
or 'hcat' functions respectively would be the way to go. However, if you want to modify
an existing array in place, then using the 'append!' function would be appropriate.

# writing type-stable code
Code is said to be type-stable if the type of every variable does not vary over time.
To clarify this idea, consider the following two closely related function definitions.

function summation1(n::Int)
    r = 0
    for i in 1:n
        r += sin(3.4)
    end
    return r
end

function summation2(n::Int)
    r = 0.0
    for i in 1:n
        r += sin(3.4)
    end
    return r
end

The difference between these function definitions is that summation1 initializes
r to 0, whereas summation2 initializes r to 0.0

julia> summation1(100_000)
-25554.110202663698

julia> summation2(100_000)
-25554.110202663698

juliaa> @time [summation1(100_000) for i in 1:1000];
  0.131536 seconds (31.90 k allocations: 2.162 MiB, 27.03% compilation time)

julia> @time [summation2(100_000) for i in 1:1000];
  0.129389 seconds (28.65 k allocations: 1.941 MiB, 25.74% compilation time)

We can confirm that the compiler produces more complex code by examining the LLVM IR
for both of these functions.

julia> code_llvm(summation1, (Int, ))
julia> code_llvm(summation2, (Int, ))

The difference in size and complexity of code between these two functions in compiled
form is considerable. And this difference is entirely attributable to the compiler's
need to recheck the type of r on every iteration of the main loop in summation1, which
can be optimized out in summation2, where r has a stable type.

Reference:
https://www.johnmyleswhite.com/notebook/2013/12/06/writing-type-stable-code-in-julia/
