# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements.  See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership.  The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License.  You may obtain a copy of the License at
#
#   http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied.  See the License for the
# specific language governing permissions and limitations
# under the License.
"""
Auto-scheduling matrix multiplication for CPU
=============================================
**Author**: `Lianmin Zheng <https://github.com/merrymercy>`_, \
            `Chengfan Jia <https://github.com/jcf94/>`_

Different from the existing :ref:`autotvm <tutorials-autotvm-sec>` which relies on
manual templates to define the search space, the auto-scheduler does not require any templates.
The auto-scheduler is template-free, so users only need to write the computation declaration without
any schedule commands or templates.
The auto-scheduler can automatically generate a large
search space and find a good schedule in the space.

We use matrix multiplication as an example in this tutorial.
"""

import numpy as np
import tvm
from tvm import te, testing, auto_scheduler

######################################################################
# Define the computation
# ^^^^^^^^^^^^^^^^^^^^^^
# To begin with, let us define the computation of a matmul with bias add.
# The function should return the list of input/output tensors.
# From these tensors, the auto-scheduler can get the whole computational graph.

@auto_scheduler.register_workload
def matmul_add(N, L, M, dtype):
    A = te.placeholder((N, L), name="A", dtype=dtype)
    B = te.placeholder((L, M), name="B", dtype=dtype)
    C = te.placeholder((N, M), name="C", dtype=dtype)

    k = te.reduce_axis((0, L), name="k")
    matmul = te.compute((N, M), lambda i, j: te.sum(A[i, k] * B[k, j], axis=k), name="matmul")
    out = te.compute((N, M), lambda i, j: matmul[i, j] + C[i, j], name="out")

    return [A, B, C, out]

######################################################################
# Create the search task
# ^^^^^^^^^^^^^^^^^^^^^^
# We then create a search task with N=L=M=128 and dtype="float32"
# If your machine supports avx instructions, you can
# - replace "llvm" below with "llvm -mcpu=core-avx2" to enable AVX2
# - replace "llvm" below with "llvm -mcpu=skylake-avx512" to enable AVX-512

target = tvm.target.Target("llvm")
task = auto_scheduler.create_task(matmul_add, (128, 128, 128, "float32"), target)

# Inspect the computational graph
print(task.compute_dag)

######################################################################
# Next, we set parameters for the auto-scheduler.
#
# * `num_measure_trials` is the number of measurement trials we can use during the search.
#   We only make 10 trials in this tutorial for a fast demonstration. In practice, 1000 is a
#   good value for the search to converge. You can do more trials according to your time budget.
# * In addition, we use `RecordToFile` to dump measurement records into a file `matmul.json`.
#   The measurement records can be used to query the history best, resume the search,
#   and do more analyses later.
# * see :any:`auto_scheduler.auto_schedule.TuningOptions`: for more parameters

tune_option = auto_scheduler.TuningOptions(
    num_measure_trials=10, measure_callbacks=[auto_scheduler.RecordToFile("matmul.json")]
)

######################################################################
# Run the search
# ^^^^^^^^^^^^^^
# Now we get all inputs ready. Pretty simple, isn't it?
# We can kick off the search and let the auto-scheduler do its magic.
# After some measurement trials, it will return the best schedule it found.

sch, args = auto_scheduler.auto_schedule(task, tuning_options=tune_option)

######################################################################
# We can lower the schedule to see the IR after auto-scheduling.
# The auto-scheduler correctly performs optimizations including multi-level tiling,
# parallelization, vectorization, unrolling and operator fusion.

print(tvm.lower(sch, args, simple_mode=True))

######################################################################
# Check correctness and evaluate performance
# ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
# We build the binary and check its correctness and performance.

func = tvm.build(sch, args)
a_np = np.random.uniform(size=(128, 128)).astype(np.float32)
b_np = np.random.uniform(size=(128, 128)).astype(np.float32)
c_np = np.random.uniform(size=(128, 128)).astype(np.float32)
out_np = a_np.dot(b_np) + c_np

ctx = tvm.cpu()
a_tvm = tvm.nd.array(a_np, ctx=ctx)
b_tvm = tvm.nd.array(b_np, ctx=ctx)
c_tvm = tvm.nd.array(c_np, ctx=ctx)
out_tvm = tvm.nd.empty(out_np.shape, ctx=ctx)
func(a_tvm, b_tvm, c_tvm, out_tvm)

# Check results
tvm.testing.assert_allclose(out_np, out_tvm.asnumpy(), rtol=1e-3)

# Evaluate execution time.
evaluator = func.time_evaluator(func.entry_name, ctx, min_repeat_ms=500)
print(
    "Execution time of this operator: %.3f ms"
    % (np.median(evaluator(a_tvm, b_tvm, c_tvm, out_tvm).results) * 1000)
)

######################################################################
# Using the record file
# ^^^^^^^^^^^^^^^^^^^^^
# During the search, all measuremnt records are dumpped into the record
# file "matmul.json". The measurement records can be used to re-apply search results,
# resume the search, and perform other analyses.

######################################################################
# Here is an example where we load the best schedule from a file,
# print the equivalent python schedule API, and build the binary again.

# Load the measuremnt record for the best schedule
inp, res = auto_scheduler.load_best("matmul.json", task.workload_key)

# Print equivalent python schedule API. This can be used for debugging and
# learning the behavior of the auto-scheduler.
print("Equivalent python schedule:")
print(task.compute_dag.print_python_code_from_state(inp.state))

# Rebuild the binary. This shows how you can apply the best schedule from a
# log file without reruning the search again.
sch, args = task.compute_dag.apply_steps_from_state(inp.state)
func = tvm.build(sch, args)

######################################################################
# A more complicated example is to resume the search.
# In this case, we need to create the search policy and cost model by ourselves
# and resume the status of search policy and cost model with the log file.
# In the example below we resume the status and do more 5 trials.

def resume_search(task, log_file):
    cost_model = auto_scheduler.XGBModel()
    cost_model.update_from_file(log_file)
    search_policy = auto_scheduler.SketchPolicy(
        task, cost_model, init_search_callbacks=[auto_scheduler.PreloadMeasuredStates(log_file)]
    )
    tune_option = auto_scheduler.TuningOptions(
        num_measure_trials=5, measure_callbacks=[auto_scheduler.RecordToFile(log_file)]
    )
    sch, args = auto_scheduler.auto_schedule(task, search_policy, tuning_options=tune_option)

# resume_search(task, "matmul.json")

######################################################################
# .. note::
#   We cannot run the line above because of the conflict between
#   python's multiprocessing and tvm's thread pool.
#   After running a tvm generated binary the python's multiprocessing library
#   will hang forever. You have to make sure that you don't run any tvm
#   generated binaries before calling auot-scheduler's search.
#   To run the function above, you should comment out all code in
#   "Check correctness and evaluate performance" section.
#
#   You should be careful about this problem in your applications.
#   There are other workarounds for this problem.
#   For example, you can start a new thread/process (with the builtin python library
#   threading or multiprocessing) and run the tvm binaries in the new thread/process.
#   This provides an isolation and avoids the conflict in the main thread/process.
#   You can also use :any:`auto_scheduler.measure.LocalRPCMeasureContext` for auto-scheduler,
#   as shown in the GPU tutorial (:ref:`auto-scheduler-conv-gpu`).


#!/usr/bin/env python
# coding: utf-8

# In[1]:

import numpy as np
import scipy.special as sci
import matplotlib.pyplot as plt
from scipy import stats # linregress
import pandas as pd
from IPython.display import Latex

# # Lecture 11:  Background of Groundwater Modeling
#
#
# _(The contents presented in this section were re-developed principally by Dr. P. K. Yadav. The original contents are from Prof. Rudolf Liedl)_
#
# ---
#
# ## Motivation ###
#
# This lecture introduces the realm of mathematical modeling realm in groundwater studies. In the previous lectures the fundamental quantities, their properties and approach to quantify them were discussed. Those information were then used to develop system equations for varieties of groundwater problems. It was discussed then that these system equations require mathematical approaches the theory for which have to be systematically discussed and understood. Groundwater modeling can then be described as the systematic use of mathematical approaches leading to solution of the groundwater problem.
#
#
# Groundwater modeling is often the first step towards understanding and solving groundwater problems/issues. Groundwater modeling is a very broad topic, this and the following lectures only introduces fundamental part of groundwater modeling. In this course we focus on groundwater flow problems.

# ## Introduction ##
#
# ### What is a Model? ###
#
# Very succinctly a  **model** is a representation, which may be an image or a description of a real system. The description can be of different form (e.g., scales), of different level of detail (e.g., conceptual versus mathematical). The **system** to be modeled can be a **real** or also **conceptual**. A very relevant example of a real system for this course is the Darcy's experiment (see figure below), in which water is made  to flow through the porous media.
#
#
# ```{figure} images/M11_f1.png
# ---
# scale: 60%
# align: center
# name: Darcy
# ---
# Darcy's experimental setup<sup>[^Darcy(1856)]</sup>
# ```
# With Darcy's experiment, one could set-up a mathematical model to relate flow rate and hydraulic gradient ($h$). As model is _only an image_ of the real system, several assumptions have to be made in it's development. At many instances the model can not be set without these assumptions. In other cases solution of the model may not be possible without these assumptions being part of the model development. Darcy‘s Law, for instance, does not provide an exact representation of flow through individual pore channels. Rather, **average** flow behaviour through **many** pore channels is represented.
#
#
# [^Darcy(1856)]: Darcy, H., Les Fontaines Publiques de la Ville de Dijon, Dalmont, Paris, 1856.
#

# ### Model Types: Process-Based and Empirical Models ###
#
# Model can be classified in many ways. The following two types of classification are a more general way to classify models:
#
# > 1. **Conceptual models**
#
# > 2. **Process-Based and Empirical Models**
#
# > 3. **Mathematical models**
#
# #### Conceptual models ####
#
# The **conceptual models** is classification of models that distinguishes the qualitative from the quantitative description of a real system. A **conceptual model** provides a qualitative representation of the relevant system components, processes, and impacts in the area of investigation. This representation is usually shown graphically, e.g., as block models (see figure {ref}`Cmodel`). It will be shown later that conceptual model in fact is the first block in the development of a mathematical model.
#
# ```{figure} images/M11_f3.png
# ---
# scale: 20%
# align: center
# name: Cmodel
# ---
# A conceptual model showing different components of a hydrological system that can impact groundwater.
# ```
#
#
# #### Physically based models and Empirical models ####
#
# The **physically based models** also referred to as **process-based** models are models that exclusively relies on the fundamental physical laws - e.g., law of conservation of mass, energy, volume. _Compartment_  based models are example of physically based models.
#
# Contrary to use of fundamental physical laws, **empirical models** are developed on the basis on experimental/collected data. Sorption isotherms that were developed in lecture [(10)](/contents/transport/lecture_10/22_reactive_transport) are types of emperical models. As was with the isotherms, these types of models are often based on regression analysis. Figure below show a prediction (blue) line as a predictor of data.
#

# In[2]:

from myst_nb import glue
import numpy as np
import matplotlib.pyplot as plt
from sklearn.linear_model import LinearRegression

# x from 0 to 30
x = 30 * np.random.random((20, 1))

# y = a*x + b with noise
y = 0.2 * x + 3.0 + np.random.normal(size=x.shape)

# create a linear regression model
model = LinearRegression()
model.fit(x, y)

# predict y from the data
x_new = np.linspace(0, 30, 100)
y_new = model.predict(x_new[:, np.newaxis])

# plot the results
fig, ax = plt.subplots(figsize=(6, 4))
ax.scatter(x, y, c="red", label = "data")
ax.plot(x_new, y_new, label = "prediction")

ax.set_ylabel(r'Discharge (m$^3$/s)')
ax.set_xlabel('Hydraulic Head (m)')

ax.axis('tight')
ax.legend()

plt.close(fig)
glue("em_fit", fig, display=False )

# ```{glue:figure} em_fit
# :figwidth: 600px
# :name: "empirical"
#
# Empirical relation between hydraulic head and discharge.
# ```
#
# In certain cases **hybrid models**, e.g. **semi-empirical** models can also occur. These models combines the components of empirical/numerical and analytical models. Darcy's law in fact is a semi-empirical model. On the one hand, it is based on the momentum conservation.  On the other hand, it is not possible to _strictly_ deduce the direct proportionality between flow rate and hydraulic gradient by averaging the flow behaviour over all pores.
#

# #### Mathematical models ####
#
# A **mathematical model** provides a quantitative representation of the relevant system components (described by for e.g., conceptual model),processes (e.g., described by physical model) and impacts in the area of investigation. The quantitative representation is based on mathematical equations. The system equations, that were developed and discussed in Lecture [(7)](/contents/flow/lecture_07/17_quantify_flow) are mathematical models.
#
# System equation or mathematical models can in certain cases be solved directly resulting to an _exact solution_ called **analytical solution**. Theis equation that was developed in Lecture [(8)](/contents/flow/lecture_08/18_wells) to quantify aquifer drawdown resulting from pumping of groundwater is an example of an analytical solution.
#
# For more complex problems, often a more natural groundwater conditions, only _approximate_ (or non-exact) solution called **numerical solution** can be  obtained. Numerical solutions are obtained after converting the system equation to so called **numerical models**.
#
# Our focus in this introductory modeling lecture is to understand the development and solution of _numerical model_ of simple groundwater flow problems.
#
#

# ### Example of an Analytical Solution ###
#
# Consider a conceptual model presented in the Figure {ref}`Ditch`. The unconfined aquifer separates two surface exposed water bodies. The water body in the left has a higher hydraulic head ($h_0$, [L]) compared to that on the right ($h_u$). Thus the flow of water is from left water body to the right one along the separating aquifer. In this scenario one of the problem to address will be to understand how the aquifer reacts to change in heads of water bodies. Additionally, how additional water, e.g., from precipitation/recharge ($N$, [L/T]), will effect the aquifer water.
#
# ```{figure} images/M11_f4.png
# ---
# scale: 20%
# align: center
# name: Ditch
# ---
# Conceptual model of a flow between two water bodies separated by unconfined aquifer
# ```
# The conceptual problem can be addressed when assumptions such as steady condition prevails, Darcy's law in aquifer is valid, recharge rate are relatively low. Based on the these assumptions, one of the **mathematical model** of this conceptual problem is:
#
# $$
# \frac{\textrm{d}}{\textrm{d}x}\bigg(-h \cdot K\cdot \frac{\textrm{d}h}{\textrm{d}x}  \bigg) = N
# $$
#
# and the 2 boundary conditions:
#
# $$
# h(0) = h_0 \:\: \text{and} \:\: h(L) = h_L
# $$
#
# Note that a complete formulation of mathematical model requires accompanying boundary conditions. The boundary conditions are used to uniquely define the problem. An _analytical solution_ for this mathematical model and accompanying boundary condition is:
#
# $$
# h(x) = \sqrt{h_o^2 - (h_o^2 - h_L^2)\cdot \frac{x}{L} + \frac{N}{K}\cdot x \cdot (L-x) }
# $$
#
# The solution can be used to quantify change in aquifer head $h$ with different system quantities, e.g., conductivity $K$ [L/T], recharge $N$ [L/T] water body heights $h_0,\, h_L$ [L] along the flow direction.
#
# The additional tool: _Conservative Transport_ ([TOOLS](/contents/tools/1D_ditchflow)) interactively simulates the ditch flow concept in more details.

# ### Example problem  ###
#
# ```{admonition} Ditch flow
# Explore the effect of recharge ($N= 0$ and $N= 0.1$ mm/d) on the aquifer level for the conceptual problem provided above. Other required data are provided below.
# The effect are to be explored at mid of the aquifer
# ```
#
#

# In[3]:

print("Provided are:\n")

K = 2E-4 # hydraulic conductivity [m/s]
Ho = 10 # head at the origin [m]
Hu = 7.5 # head at L [m]
L = 175 #flow length [m]
N1 = 0 # no recharge [m/s]
N2 = 1000 # recharge [mm/a]

# intermediate calculation
x = L/2 # mid of the aquifer [m]
N_ = N2/1000/365/86400 # recharge, [m/s]

#solution
h1=(Ho**2-(Ho**2-Hu**2)/L*x+(N1/K*x*(L-x)))**0.5
h2=(Ho**2-(Ho**2-Hu**2)/L*x+(N_/K*x*(L-x)))**0.5

print("hydraulic conductivity = {} m\nhead at origin = {} m\nhead at L = {} m\nflow length = {} m\nRecharge = {} mm/a".format(K, Ho, Hu, L, N2 ),"\n")
print("The resulting head without head is {:0.2f} m \n".format(h1))
print("The resulting head with head is {:0.2f} m \n".format(h2))

# ### Example for a Model without Analytical Solution ###
#
# Analytical solutions are rather an exception. Natural aquifer or groundwater system are more complex (see figure below) and therefore analytical solution are not possible. The complexity in natural system are due to parameter heterogeneity and irregular model domain boundaries, and these must be included in underlying model equation.
#
#
# ```{figure} images/M11_f5.png
# ---
# scale: 60%
# align: center
# name: nummodel
# ---
# The numerical model of a natural aquifer
# ```
#
#
#

# ## Conceptual Model to Numerical Approach ##
#
# The first step in modeling is to establish the purpose of the model. With that established, the development of conceptual model begins the set-up of the numerical model. This is a step-wise process that includes:
#
# > 1. **Conceptual model** - providing hydrogeological units within the model domain
#
# > 2. **Water budgeting** - identifying water containing units and characterizing it
#
# > 3. **Numerical model** - Combining the units of conceptual model, water budgeting components and imposing _boundary conditions._
#
# ```{figure} images/M11_f6.png
# ---
# scale: 60%
# align: center
# name: nummodel
# ---
# The numerical model of a natural aquifer
# ```
#
# The water budgeting part should include the sources of water inflowing to the system including the expected flow directions and exiting water. Estimate of Groundwater recharge, overland flow etc, are few examples of inflows. Likewise, estimate of baseflow to streams, evapotranspiration, abstraction wells etc. are outflows. Field data are required to prepare water budget.
#
# The type of numerical model, depending on the modeling goal and required/available data, is then decided. The model type is mostly time-related (transient, steady) and dimension-related (1D,2D, 3D).

# ### Data Requirements ###
#
# Numerical model are often very data intensive. Several types of data of different origin are required in the development of a numerical model. These data come from site data records/maps, field works, lab works and the ancillary mathematical analysis of the field and lab data. Overall, the required data can be:
#
# + topographical maps (with surface waters and water divides)
# + geological maps, geological profiles (see figure {ref}`nummodel` example)
# + maps with isolines of aquifer bottoms / thicknesses , aquitard bottoms / thicknesses
# + maps indicating vertical extensions of sediments under rivers and lakes
# + hydrogeological maps (hydraulic head isolines)
# + water level time series in observation wells and rivers
# + time series of spring discharges
# + maps and profiles of hydraulic conductivity or transmissivity (also for river / lake sediments mentioned above)
# + maps and profiles of storage coefficients
# + information on spatial and temporal variability of inflow / outflow due to
#   - groundwater recharge
#   - evapotranspiration
#   - interaction between groundwater and surface water
#   - groundwater abstraction
#   - natural groundwater flow
#

# ## Example of a Groundwater Model ##
#
# Let us now develop an example flow model. To begin with we set-up an overly simplified model that still will require a numerical solution.
#
# **Step 1** - Spatial Extension
# + Horizontal extension along $x-$direction: 4000 m
# + Horizontal extension along $y-$direction: 2500 m
# + vertical extension along $z$-direction: from $z = 250$ m a.s.l. at the aquifer bottom to $z= 265$ m at the aquifer top.
# + aquifer thickness is uniform = 15 m.
#
# Putting these information graphically, we get the following schematic:
#
# ```{figure} images/M11_f7.png
# ---
# scale: 40%
# align: center
# name: nummodel_ex
# ---
# Spatial extension of an example numerical model
# ```
# The example model is 2D and we _assume_ that vertical flow components can be neglected despite the recharge vertically entering the groundwater. These kinds of simplification are quite common in the development of the numerical model. These simplifications have to be justified when presenting model results.
#
# **Step 2** - Hydraulic Properties
#
# For our example model we consider the following:
#
# + effective porosity ($\eta_e$) in the model domain: 0.2 or 20%
# + two zones with different hydraulic conductivities ($K$)
# + two zones with different groundwater recharge
# + A section of a river (_river reach_) is in hydraulic contact with the aquifer. i.e. there may be water transfer from the river to the aquifer (_influent conditions_) or vice versa (_effluent conditions_).
# + inflow boundary with prescribed hydraulic heads
# + outflow boundary with prescribed hydraulic heads
# + two impermeable boundaries
#
# **Step 3** - The model purpose and conceptual model
#
# From our example model we intend to:
#
# + Abstraction of groundwater through wells is planned in the area with an overall pumping rate of 7000 m$^3$/d.
#
# + Water extraction is to be distributed between two wells located at $(x,y) =$ (3050 m, 1550 m) and $(x,y)$ = (3050 m, 1450 m), resp.
#
# + The model purpose is to outline the 50-days isochrone for both wells.
#
# With these information available, our conceptual model takes the following form:
#
# ```{figure} images/M11_f8.png
# ---
# scale: 25%
# align: center
# name: Concept_model
# ---
# The conceptual model of the example model
# ```
# **Step 4** - The numerical approach
#
# This is discussed in the next lecture.

#


#!/usr/bin/env python
# coding: utf-8

# In[1]:

import numpy as np
import matplotlib.pyplot as plt
import pandas as pd
import ipysheet as ips
import panel as pn
from scipy import stats
pn.extension("katex")

# # Tutorial 4 #
#
# + **solutions for homework problems 1 – 4**
#
# + **tutorial problems on effective conductivity and flow nets**
#
# + **homework problems on effective conductivity and flow nets**
#
#
#
#
#
#
#
# ### Solutions for Homework Problems 1 – 2 ###
#
#
#

# In[2]:

#
r1_1 = pn.pane.Markdown("""

### Homework Problem 1 ###

The pressure head in an aquifer extending over 200 km2 is decreased by 1.60 m.
Determine the loss of groundwater in the aquifer for two scenarios:
A. The aquifer is unconfined (storage coefficient 0.13).
B. The aquifer is confined (storage coefficient 0.0005).

""",width = 800, style={'font-size': '13pt'})

r1_2= pn.pane.PNG("images/T03_H1.png", width=350)
#r1_2 = pn.pane.PNG("images/T03_H1.PNG")

### Tutorial Problem 7 – Solution ###

#

r1_3 = pn.pane.Markdown("""
### Solution - Homework Problem 1 ###

Relevant information can be found in Lecture L03, Slides- 28-30

""",width = 800, style={'font-size': '13pt'})

r1_3b = pn.pane.LaTeX(r"""

The relevant equations is:

$$
S = \Delta V_w/(A\cdot \Delta H)
$$

""",width = 800, style={'font-size': '13pt'})
pn.Column(r1_1, r1_2, r1_3, r1_3b)

# In[3]:

# Given
A = 200 # km^2, aquifer area
D_h = 1.6 # m, head decrease
S_u = 0.13 # (-), Storativity unconfined aquifer
S_c = 0.0005 # (-) Storage coefficient, confined aquifer

# Solution
DV_wu = A*S_u*D_h * 10**6 # m^3 change in water volume unconfined aquifer
DV_wc = A*S_c*D_h* 10**6  # m^3 change in water volume unconfined aquifer

# output

print("Change in water volume in unconfined aquifer is: {0:1.1e}".format(DV_wu),"m\u00b3 \n")
print("Change in water volume in confined aquifer is: {0:1.1e}".format(DV_wc),"m\u00b3")

# ## Homework Problem 2
#
# Conduct a sieve analysis for a dried soil sample (see data in the table below)
#
# 1. Draw the granulometric curve (cumulative mass distribution) and briefly characterise the sediment with regard to its major constituent(s).
# 2. What is the coefficient of uniformity?
#

# In[4]:

#
title = ["mesh   size  [mm] ", "residue in the sieve [g] ", "∑Retained %", "Commulative Passed %"]
Size = [6.3, 2, 0.63, 0.2, 0.063, "< 0.063 /cup"]
passed = [11, 62, 288, 189, 42, 10]
s2 = ips.sheet(rows=6, columns=4, row_headers=False, column_headers=title)
ips.column(0, Size, row_start=0)
ips.column(1, passed, row_start=0); s2

# In[5]:

# Solution of problem 2

t_sample = np.sum(passed) # g, add the residue column to get total mass
retained_per = passed/t_sample *100 # %, # retain percentage residue/total mass
retain_per_cumsum =np.cumsum(retained_per) # get the cummulative sum of the reatined
passing_cumper = 100 - retain_per_cumsum # substract 100-cummsum to get passing % - the last column

#Output
s3 = ips.sheet(rows=6, columns=4, row_headers=False, column_headers=title)
ips.column(0, Size, row_start=0)
ips.column(1, passed, row_start=0);
ips.column(2, retained_per, row_start=0);
ips.column(3, passing_cumper, row_start=0); s3

# In[6]:

# Plotting granulometric curve

plt.rcParams['axes.linewidth']=2
plt.rcParams['grid.linestyle']='--'
plt.rcParams['grid.linewidth']=1
x = np.append([20], Size[:5]) # adding for all left over.
y = np.append([100],passing_cumper[:5])
fig = plt.figure(figsize=(9,6));
plt.plot(x, y, 'x-', color='red', lw=2.5);
tics=x.tolist()
plt.xscale('log');lw=2.5
plt.grid(which='major', color='k', alpha=0.7)
plt.grid(which='minor', color='k', alpha=0.3)
plt.xticks(x, tics);
plt.yticks(np.arange(0,110,10));
#plt.title('grain size distribution (combined wet sieving and sedimentation analysis)');
plt.xlabel('grain size d [mm]');
plt.ylabel('Cummulative Passed fraction %');

plt.annotate('', xy=(0.20, 10),  xycoords='data', xytext=(0.045, 10), arrowprops=dict(arrowstyle='->', color="b", lw=2.5),ha='right', va='top',)
plt.annotate('', xy=(1.1, 60),  xycoords='data', xytext=(0.045, 60), arrowprops=dict(arrowstyle='->', color="b", lw=2.5),ha='right', va='top',)
plt.annotate(r'$d_{60}$', xy=(1, 60),  xycoords="data", xytext=(0.85, -3),color='red',size=12, arrowprops=dict(arrowstyle='<-', color="b", lw=2.5),ha='left', va='bottom',)
plt.annotate(r'$d_{10}$', xy=(0.20, 10),  xycoords='data', xytext=(0.235, 1.5),color='red',size=12, arrowprops=dict(arrowstyle='<-', color="b", lw=2.5),ha='right', va='top',)
plt.rcParams["font.weight"] = "bold"

plt.savefig("fig6.png")

mpl_pane = pn.pane.Matplotlib(fig, dpi=144)

# In[7]:

# From the figure
d_10 = 0.22 # mm,approx, diameter 10% passing, see the arrow bottom in x-axis
d_60 = 1.0 # mm, approx diameter 10% passing, see the arrow bottom in x-axis

c_u = d_60/d_10 # [], coefficient of uniformity

#Output
print("The coefficient of uniformity is: {0:1.1f}".format(c_u))
r2_1 = pn.pane.Markdown("""
**Major constituents: coarse sand/medium sand** """, width=600, style={'font-size': '13pt', 'color': 'blue'} )
pn.Row(r2_1)

# In[8]:

# Tutorial Problem 11- Effective Conductivity and flow nets
r5_1 = pn.pane.Markdown("""
#Tutorial Problems on Effective Conductivity and Flow Nets
""", width = 900)

r5_2 = pn.pane.Markdown("""
###Tutorial Problem 11: Effective Hydraulic Conductivity
A sandy layer with a thickness of 2.5 m is embedded between two gravel layers. B
oth gravel layers have a thickness of 1.5 m and a hydraulic conductivity of 3.7·10-3 m/s.
Steady-state groundwater flow is in parallel to the layering.
A hydraulic gradient of 0.001 and an overall discharge of 1 m³/d per unit width have been determined.

a. Determine the effective hydraulic conductivity.

b. What is the hydraulic conductivity of the sand layer?

c. Which effective hydraulic conductivity would be obtained if flow was assumed perpendicular to the layering?

d. Calculate effective hydraulic conductivity if the angle between the flow direction and the layering equals 45°.
""", style={'font-size': '13pt'})

pn.Column(r5_1, r5_2)

# In[9]:

# Solution of Problem 11
r5_3 = pn.pane.PNG("images/T03_TP11_a.png", width=400)
r5_4 = pn.pane.LaTeX(r"""
Known relationships are (see Lecture 05, Slides 8-13, 22):
$$
Q = WmK\frac{\Delta H}{L}
$$
$$
K = \frac{Q/W}{m\cdot \Delta H \cdot L}
$$
Weighted arithmetic mean to determine hydraulic conductivity for sand:

$$
K = \frac{1}{m}\sum_{i=1}^n m_i\cdot K_i
$$
where $i$ is different layers
""", width = 500, style={'font-size': '13pt'})
spacer2 = pn.Spacer(width=100)

pn.Row(r5_3, spacer2, r5_4)

# In[10]:

#Given Solution of 11 a, b

Q = 2 # m^3/d, discharge
W = 1 # m, per unit width
K_g = 3.7*1E-3# m/s, conductivity of gravel layer
m_g = 1.5 # m, thickness of gravel layer
m_s = 2.5 # m, thickness of sand layer
m = 2*m_g + m_s # m. total thickness of aquifer
Dh_L = 0.001 # (-), hydraulic gradient

#Solution of 11a
Keff_h = (Q/W)/(m*Dh_L) # m/d, conductivity
Keff_hs = Keff_h/(24*3600)# m/s, conductivity unit changed

#Solution of 11b
# K_eff = (2*m_g*K_g + m_s*K_g)/m

K_s = ((m*Keff_hs - 2*m_g*K_g))/m_s

print("Effective horizontal hydraulic conductivity (Keff_h) = {0:1.2f}".format(Keff_h), "m/d\n" )
print("Effective horizontal hydraulic conductivity (Keff_hs) = {0:1.3E}".format(Keff_hs), "m/s\n" )
print("Hydraulic conductivity of sand layer (K_s) = {0:1.1E}".format(K_s), "m/s" )

# In[11]:

#Given Solution of 11 c, d

r5_5 = pn.pane.PNG("images/T03_TP11_b.png", width=200)
r5_6 = pn.pane.PNG("images/T03_TP11_c.png", width=200)

r5_7 = pn.Column(r5_5, r5_6)

r5_8 = pn.pane.LaTeX(r"""
Vertical effective conductivity is given by weighted harmoninc mean
$$
K = \frac{m}{2\cdot \frac{m_g}{K_g} + \frac{m_s}{K_s} }
$$

For inclined aquider the effective conductivity is:

$$
K = \frac{1}{\frac{\cos^2\theta}{K_h} + \frac{\sin^2\theta}{K_v}}
$$

""", style={'font-size': '13pt'})

pn.Row(r5_7,spacer2, r5_8)

# In[12]:

# Solution of 11c

Keff_v = m/(2*(m_g/K_g)+ (m_s/K_s))

#Given
theta = 45 # theta
theta_r = 45*(np.pi)/180 # degree to radian conversion
K_h = Keff_hs # m/s, solution from 11a
K_v = Keff_v # m/s, solution from 11c

# solution from 11d
Keff_i = 1/((np.cos(theta_r)**2/K_h)+(np.sin(theta_r)**2/K_v))

print("Effective vertical hydraulic conductivity (Keff_v) = {0:1.2E}".format(Keff_v), "m/s\n" )
print("Effective inclined hydraulic conductivity (Keff_i) = {0:1.2E}".format(Keff_i), "m/s" )

# In[13]:

#
r6_1 = pn.pane.Markdown("""
### Tutorial Problem 12: Hydrologic Triangle
The figure below shows the position of four groundwater observation wells with measured hydraulic heads in m a.s.l.

**a.** Sketch head isolines for intervals of 1 m by applying the hydrologic triangle method.

**b.** Indicate the flow direction.

""",width = 400, style={'font-size': '13pt'})

r6_2 = pn.pane.PNG("images/T03_TP12_a.png", width=400)

pn.Row(r6_1,spacer2, r6_2)

# In[14]:

#
r6_3 = pn.pane.Markdown("""
### Solution of Tutotrial Problem 12

Step 1. Connects all the points
""", width=600)

r6_2.object = "images/T03_TP12_b.png"
r6_3

# In[15]:

#
r6_4 = pn.pane.Markdown("""
### Solution of Tutotrial Problem 12
Step 2. Divide the connected lines at equal head-level (here = 1 m)
""", width=600)
r6_2.object = "images/T03_TP12_c.png"

# In[16]:

#
r6_4 = pn.pane.Markdown("""
### Solution of Tutotrial Problem 12
Step 3. Join all the equal head lines
""", width=600)
r6_2.object = "images/T03_TP12_d.png"

# In[17]:

r6_4 = pn.pane.Markdown("""
### Solution of Tutotrial Problem 12
Step 4. Mark the flow direction from higher head towards lower head
""", width=600)
r6_2.object = "images/T03_TP12_e.png"

# In[18]:

#
r7_1 = pn.pane.Markdown("""
##Tutorial Problem 13: Flow Nets##

Sketch head isolines and streamlines for the two configurations a) and b) of a well doublette shown below. In both cases flow nets should be sketched without and with the uniform flow component.

""",width=800,  style={'font-size': '13pt'})

r7_2 = pn.pane.Markdown("""
 a) withdrawal at both wells:

""",width=400,  style={'font-size': '13pt'})

r7_3 = pn.pane.PNG("images/T03_TP13_a.png", width=200)

r7_4 = pn.Column(r7_2,r7_3)

r7_5 = pn.pane.Markdown("""
 b) Injection and withdrawl wells:

""",width=400,  style={'font-size': '13pt'})

r7_6 = pn.pane.PNG("images/T03_TP13_b.png", width=200)

r7_7 = pn.Column(r7_5,r7_6)
r7_8 = pn.Row(r7_4, r7_7)
pn.Column(r7_1, r7_8)

# In[19]:

r8_1= pn.pane.Markdown("""
#Homework Problems on  Effective Conductivity and Flow Nets

""", width = 800, style={'font-size': '13pt'})

r8_2= pn.pane.Markdown("""
#There is no obligation to solve homework problems!
""", width = 800, style={'font-size': '13pt', 'color':'red'})

pn.Column(r8_1,r8_2)

# In[20]:

#
r9_1= pn.pane.Markdown("""
###Homework Problem 5: Effective Hydraulic Conductivity
A gravel layer with a thickness of 2.5 m is embedded between two sand layers. Both sand layers have a thickness of
1.5 m and a hydraulic conductivity of 3.7·10-4 m/s. Steady-state groundwater flow is perpendicular to the layering.
An overall head difference of 5.5 cm and a discharge of 500 l/d per unit area have been determined

**a.** Determine the effective hydraulic conductivity.

**b.** What is the hydraulic conductivity of the gravel layer?

**c.** Which effective hydraulic conductivity would be obtained if flow was assumed to be in parallel with the layering?

**d.** Calculate effective hydraulic conductivity if the angle between the flow direction and the layering equals 30°.

""", width = 900, style={'font-size': '13pt'})
r9_1

# In[21]:

#
r10_1= pn.pane.Markdown("""
###Homework Problem 6: Hydrologic Triangle
The figure below shows the position of five groundwater observation wells with measured hydraulic heads in m a.s.l.

**a.** Sketch head isolines for intervals of 1 m by applying the hydrologic triangle method.

**b.** Indicate the flow direction.

""", width = 500, style={'font-size': '13pt'})
r10_2 = pn.pane.PNG("images/T03_TH6.png", width=400)

pn.Row(r10_1, r10_2)

# In[22]:

#
r11_1= pn.pane.Markdown("""
###Homework Problem 7: Flow Nets
Sketch head isolines and streamlines for the well doublette shown below.
In this case, injection and withdrawal of groundwater is superimposed to a uniform flow component.

 """, width = 900, style={'font-size': '13pt'})

r11_2 = pn.pane.PNG("images/T03_TH7.png", width=400)

r11_3= pn.pane.Markdown("""

 """, width = 900, style={'font-size': '13pt'})
pn.Column(r11_1, r11_2, r11_3)

# In[ ]:


#!/usr/bin/env python
# coding: utf-8

# # Lab 1 Solutions
#
# ### Objectives
# In this lab, we'll
# - Review the computational infrastructure around our data science environments,
# - Go through the process of ensuring that we have a Python environment set up for this class with the proper installed packages
# - Within our environment, we'll review the basic data science operations in Python, and introduce some tips and tricks.
#
#
# ```{admonition} Take a Deep Breath
# Don't freak out if stuff presented here is brand new to you! Ask a friend, google around (esp. stack overflow) and find a solution. All of these examples can be done in a few lines of code.
# ```
#
# # Part I: Computational Ecosystem
#
# In the space below, or in your own assignment, answer the following:
#
# ## Question A
#
# Describe the following terms, and point out the differences between them. Feel free to look things up.
# - python:
# - the terminal:
# - the file system:
# - jupyter:
# - an IDE:
# - a text editor:
# - git:
# - PATH:
#

# I am writing this lab in a notebook. We'll be discussing the pros and cons of notebooks in class. Below, I'm going to check which python installation on my computer is being pointed to within my PATH. As a reminder, you can see your full path by echoing it from the terminal. Within a notebook, that looks like this:

# In[2]:

get_ipython().system('echo $PATH')

# ```{note}
# The "!" in my notebook allows me to run terminal commands from a notebook; you don't need this symbol when running commands in an actual terminal.
# ```

# I can check my python as follows:

# In[3]:

get_ipython().system('which python')

# We can see that calls to `python` are triggering the python installed in `anaconda3`, which is what we want (see the installation video for more details). If your call to `which python` in the terminal returns something like `usr/bin/python`, then something has likely gone wrong with your installation. There are some troubleshooting steps suggested in the installation video.
#
# ## Setting up an Environment.
#
# We can think of packages as programs installed on our computer. But what if one of my projects needs Photoshop 14.0, and another needs a feature that was only available in Photoshop 12.5.2? An environment is the system on which your code is being executed. When you fire up a terminal, this is usually your *base* environment, the default one for your *user* on a given computer system. But rather than always installing programs in this base installation, we can create custom environments for each of our projects. We can then install the exact dependencies for those projects within our environments, and we'll know they won't mess with each other.
#
# For this class, we're going to be doing a lot of package installations. To ensure we are all on the same page and are working with the same tools, we're going to use a `conda environment` to maintain versioning. Note: there are multiple environment creation tools/methods. Conda environments are the predominant standard in astronomy, hence their use here.
#
# In your terminal, type the following:
# ```{note}
# If you are on WINDOWS, you NEED to use the ANACONDA TERMINAL. NOT YOUR WINDOWS POWER SHELL/COMMAND PROMPT. Search your pc for anaconda and you'll see an anaconda launcher (if you followed the installation video correctly). From there, you should be able to find an anaconda terminal/prompt. That's where you should do anything whenever I say "from your terminal".
# ```
#
#

# In[ ]:

conda create -n a330 python=3.8

# Once you run this, answer "y" to the prompts, and your new environment will be installed.
#
# ```{note}
# The above command may take several minutes to execute.
# ```
#
#
# Next, we want to activate this environment (still in our terminal). We do this as follows:

# In[ ]:

conda activate a330

# When you do so, you should see the left hand edge of your prompt switch from (base) to (a330).
#
# Next, let's make an alias so that getting into our a330 environment is a snap. We're going to access a file called `.bash_profile`, which allows us to set aliases and environment variables. This file is located in your home directory, so I can print mine here:

# In[6]:

get_ipython().system('more ~/.bash_profile')

# Notice above I use the `~` which is a shorthand for home directory. On my computer, the default home directory for my user is `/Users/ipasha/`.
#
#
#
# This file has some conda stuff in it at the top, as well as some path and python path exports, as well as an alias.
#
# Yours should also have the conda init stuff, if you installed anaconda properly. Using your text editor of choice, add a line to this file that reads `alias a330='conda activate a330'`.
#
# ```{sidebar} Using Vi/vim
# Vi/vim is a built-in terminal program that allows for the editing of files. It is helpful to learn, especially when working on remote servers. We'll go into it more later, but here is a step by step for performing the above step with vim.
# - First: from the terminal, type `vim ~/.bash_profile` and hit enter. This will open the editor. If 'vim' isn't recognized, try 'vi'.
# - Next: Press the "I" key to open insert mode. Move your cursor with the arrow keys to the desired line, then type in the alias command shown to left.
# - Finally: Press `esc` to get out of insert mode, then type `:wq` and hit enter in order to "write" then "quit".
# ```
#
#
#
# Now, from your terminal, source your profile by typing `source ~/.bash_profile`. You're good to go! Test that you can activate your environment by typing `a330` and hitting enter.
#
# ```{note}
# To deactivate, just type `conda deactivate`.
# ```
#
#

# ## Adding Jupyter
#
# You'll be using notebooks during this class, and we need to make sure that we can access our new environment from within Jupyter notebook. To ensure this, we're going to do the following:
#
# First, make sure your environment is activated.
#
# Then, type:

# In[ ]:

conda install -c anaconda ipykernel

# This ensures we can select different kernels inside jupyter. A kernel is basically "the thing that is python", the root thing being run on your system when you use python. By creating environments, we're creating different unique kernels, and we can now get to them within our notebooks.
#
# Now, run the following:

# In[ ]:

python -m ipykernel install --user --name=a330

# Once you've done this, you should have the ability to access your new environment from within Jupyter. We can test this as follows:
# - First, open a new terminal window, and activate your environment (if you made the alias, this means typing `a330` in your terminal.
# - Next, type `jupyter lab` to open jupyter lab. If for some reason you don't have jupyter lab yet, you can install it now with `conda install -c conda-forge jupyterlab`.
# - Once you have lab open, there should be a 'launcher' page, with one option being to create a new notebook using python -- you *should* see your environment listed there.
# - If you don't hit refresh on the webpage just in case.
# - You can also click on the option to open a python3 notebook. Inside, in the top right corner, it should say your current environment (probably Python 3). Clicking that, it should give you the option to choose a different environment, and your environment should be listed there.
#
# ```{note}
# If you already had a lab open, you'll have to hit refresh to get it to show up.
# ```
# ## Installing Packages
#
# Now that we have our environment, we're going to install the set of packages we need for this class. We may need more of them as the semester goes on, but for now, do the following (in your terminal, within your environment).

# In[ ]:

conda install -n a330 numpy scipy astropy matplotlib

# (again, hitting "y" when prompted). Again, this step might take a minute or so to run.
#
# Congrats, you now have an environment set up for this class, and can jump in and out of it at will, either in your terminal, or within a Jupyter notebook.
#
# ```{admonition} Hot Tip
# It's highly recommended you do these steps anytime you start a new research project. Up front, you may not know all the dependencies that will arise, but as you go along, if you keep your work to that environment, you'll be able to carefully control which versions of which packages you're accessing at all times.
# ```
#
#

# # Part II: Python Review
#
# In this section, I'll ask you to perform some pythonic operations to get back into the swing of things if it has been a little while.
#
# For this assignment, please carry out your work in a Jupyter notebook, with the questions labeled and your output shown. You'll submit this notebook via Github, but we will discuss how to perform this step in class.

# ## Question 1
# Create a 2D array of dimensions 1000 x 1000, in which the values in each pixel are random-gaussian distributed about a mean of 10, with a sigma of 2, and then use matplotlib to display this image. Make sure (0,0) is in the lower lefthand corner.

# In[145]:

import numpy as np
import scipy.stats as stats
import matplotlib.pyplot as plt

# In[146]:

# Solution 1
mu = 10
sigma = 2
Gauss = stats.norm(loc=mu,scale=sigma)
array = Gauss.rvs((1000,1000))
fig, ax = plt.subplots(figsize=(10,10))
ax.imshow(array,origin='lower');

# In[147]:

#Solution 2:
array_np = np.random.normal(loc=10,scale=2,size=(1000,1000))
fig, ax = plt.subplots(figsize=(10,10))
ax.imshow(array_np,origin='lower');

# ## Question 2
#
# The distribution of pixels in your above image should not have many outliers beyond 3-sigma from the mean, but there will be some. Find the location of any 2 sigma outliers in the image, and highlight them by circling their location.
# Confirm that the fraction of these out of the total number of pixels agrees with the expectation for a normal distribution.

# In[149]:

# Solution
outliers = np.where((array>mu+3*sigma)|(array<mu-3*sigma))
fig, ax = plt.subplots(figsize=(10,10))
ax.imshow(array,origin='lower')
ax.plot(outliers[0],outliers[1],'o',ms=5,color='None',mec='r');

# ## Question 3
#
# When dealing with astronomical data, it is sometimes advisable to not include outliers in a calculation being performed on a set of data (in this example, an image). We know, of course, that the data we're plotting ARE coming from a gaussian distribution, so there's no reason to exclude, e.g., 3-sigma outliers, but for this example, let's assume we want to.
#
# Create a numpy masked array in which all pixels that are > 3$\sigma$ from the image mean are masked. Then, calculate the mean and sigma of the new masked array.

# In[34]:

# Solution
clipped_array = np.ma.masked_where((array>mu+3*sigma)|(array<mu-3*sigma),array)
print(f'Clipped Mean: {np.mean(clipped_array):.3f}  |  Clipped Sigma: {np.std(clipped_array):.3f}')

# In[154]:

# solution 2
m= (array < mu+3.*sigma) & (array>mu-3.*sigma)

mn = np.mean(array[m])
st = np.std(array[m])
print('Clipped mean; {:0.3f};  Clipped Sigma: {:0.3f}'.format(mn,st))

# As expected, clipping the outliers of this distribution does not affect the mean in any strong way, but does noticably decrease $\sigma$.

# ## Question 4:
#
# Using Array indexing, re-plot the same array from above, but zoom in on the inner 20% of the image, such that the full width is 20% of the total. Note: try not to hard code your indexing. You should be able to flexibly change the percentage. For this one, use a white-to-black color map.
#

# In[150]:

# solution
cent = int(array.shape[0] / 2)
perc = int(0.2 * array.shape[0]*0.5)
cropped_array = array[cent-perc:cent+perc,cent-perc:cent+perc]
fig, ax = plt.subplots(figsize=(10,10))
ax.imshow(cropped_array,origin='lower',cmap='gray_r');

# As expected, our image is now 200 by 200 pixels across. Note that our new image has its own indexing. A common "gotcha" when working with arrays like this is to index in, but then try to use indices found (e.g., via `where()`) in the larger array on the cropped in version, which can lead to errors.

# ## Question 5
#
# Often, we have an expression to calculate of the form
#
# $$
# \sum_i \sum_j a_i b_j
# $$

# Your natural impulse for coding this double sum might look like this:

# In[ ]:

total = 0
for i in a:
    for j in b:
         total+= i*j

# which, mathematically, makes sense! But as it turns out, there's a way we can do this without any loops at all --- and when $\vec{a}$ and $\vec{b}$ get long, this becomes hugely important in our code.
#
# The trick we're going to use here is called [array broadcasting](https://numpy.org/doc/stable/user/basics.broadcasting.html), which you can read about at the link if you're not already familar. I'm going to give you $\vec{a}$ and $\vec{b}$ below. For this exercise, calculate the double sum indicated above without the use of a for-loop.
#
# ```{admonition} Hint
# The command `np.newaxis` will be useful here, or for a slightly longer solution, try `np.repeat` and `reshape()`.
# ```

# In[35]:

a = np.array([1,5,10,20])
b = np.array([1,2,4,16])

# Solution
output = np.sum(a[:,np.newaxis]*b)

# In[37]:

output

# We can confirmed the above worked using our slow loop:

# In[40]:

total = 0
for i in a:
    for j in b:
         total+= i*j
total

# We can also perform this trick without knowing about `np.newaxis` explicitly. You may have done it this way.

# In[55]:

matrix_a = np.repeat(a,len(b)).reshape(len(a),len(b))
matrix_b = np.repeat(b,len(a)).reshape((len(b)),len(a)).T
output = np.sum(matrix_a*matrix_b)
output

# I want to take a moment here and really highlight the difference in speed between these two methods. Let's bump up the length... by a lot:

# In[68]:

a = np.random.random(5000)
b = np.random.random(5000)

# In[69]:

get_ipython().run_cell_magic('timeit', '', '\ntotal = 0\nfor i in a:\n    for j in b:\n         total+= i*j')

# We can see that above, it took 5 whole seconds to run the cell. That's for only 5000 entries in each list. Many astronomical lists on which we might have to do this are 10,000 or more long! Notice this is an exponential operation. Try running the above on your computer with 10000 long lists. I'll wait... until you get bored and kill the process after many minutes.
#
# Now see below:

# In[70]:

get_ipython().run_cell_magic('timeit', '', 'output = np.sum(a[:,np.newaxis]*b)')

# Wow. 52 ms to do that whole thing. Broadcasting. We love it, we use it, we love it.
#
# ```{admonition} Takeaway Point
# The takeaway from this lesson should be that anytime we can do something without a loop... WE SHOULD.
# ```
#
#

# ## Question 6
#
# Often in astronomy we need to work with grids of values. For example, let's say we have a model that describes some data, and the model has 2 parameters, $a$ and $b$.
#
# We might choose different combinations of $a$ and $b$, and determine a metric for how well models of such combinations fit our data (e.g., $\chi^2$).
#
# We may then want to plot this $\chi^2$ value for each point on our grid -- that is, at each grid position corresponding to some $a_i$ and $b_j$.
#
# Below, I provide a function, `chi2`, which returns a single number given some singular inputs `a` and `b`.
#
# Create some arrays of `a` and `b` to test that range between 1 and 25, and have 10 entries evenly spaced between those values. Then, loop over them and find the $\chi^2$ using my function.
# ```{note}
# We can't get around the double loop in this case, because we are operating under the assumption that the calculation of some single $\chi^2$ using a unique combination of $a_i$ and $b_j$ cannot be vectorized. If it could, we wouldn't need to do this activity. But often, we can't, because the creation of a model given some inputs is nontrivial.
# ```
#
# Once you've stored the $\chi^2$ values for each combination of $a$ and $b$, create a plot with $a$ and $b$ as the axes and show using colored circles the $\chi^2$ value at each location. Add a colorbar to see the values being plotted.
#
# To create this grid, use the `np.meshgrid()` function. For your plot, make sure the marker size is big enough to see the colors well.
#
#
#
#

# In[151]:

def chi2(a,b):
    return ((15-a)**2+(12-b)**2)**0.2 #note, this is nonsense, but should return a different value for each input a,b

# Solution
a = np.linspace(1,25,10)
b = np.linspace(1,25,10)
chi2_values = []
for i in a:
    for j in b:
        chi2_values.append(chi2(i,j))

chi2_values = np.array(chi2_values)

xx,yy = np.meshgrid(a,b)

fig, ax = plt.subplots(figsize=(12,10))

im = ax.scatter(xx,yy,c=chi2_values,marker='o',s=100)
plt.colorbar(im);

# ## Question 7
#
# Re-show your final plot above, making the following changes:
#
# - label your colorbar as $\chi^2$ using latex notation, with a fontsize>13
# - Make your ticks point inward and be longer
# - Make your ticks appear on the top and right hand axes of the plot as well
# - If you didn't already, label the x and y axes appropriately and with a font size > 13
# - Make sure the numbers along the axes have fontsizes > 13
#

# In[152]:

fig, ax = plt.subplots(figsize=(12,10))

im = ax.scatter(xx,yy,c=chi2_values,marker='o',s=100)
ax.tick_params(direction='in',right=True,top=True,length=7,width=1.5,labelsize=14)
ax.set_xlabel(r'parameter $a$',fontsize=15)
ax.set_ylabel(r'parameter $b$',fontsize=15)
cbar = plt.colorbar(im)
cbar.set_label(r'model $\chi^2$',fontsize=14);

# ## Question 8
#
# Some quick list comprehensions! For any unfamilar, **comprehensions** are pythonic statements that allow you to compress a for-loop (generally) into a single line, and usually runs faster than a full loop (but not by a ton).
#
# Take the for-loop below and write it as a list comprehension.

# In[119]:

visited_cities = ['San Diego', 'Boston', 'New York City','Atlanta']
all_cities = ['San Diego', 'Denver', 'Boston', 'Portland', 'New York City', 'San Francisco', 'Atlanta']

not_visited = []
for city in all_cities:
    if city not in visited_cities:
        not_visited.append(city)

print(not_visited)

# In[121]:

# Solution

not_visited = [i for i in all_cities if i not in visited_cities]
print(not_visited)

# Next, create an array of integers including 1 through 30, inclusive. Using a comprehension, create a numpy array containing the squared value of only the odd numbers in your original array. (*Hint, remember the modulo operator*)

# In[122]:

# Solution
full = np.arange(1,31)
squared_odds = np.array([i**2 for i in full if i%2!=0])
print(squared_odds)

# In the next example, you have a list of first names and a list of last names. Use a list comprehension to create an array that is a list of full names (with a space between first and last names).

# In[123]:

first_names = ['Bob','Samantha','John','Renee']
last_names = ['Smith','Bee','Oliver','Carpenter']

# Solution
full_names = [i+' '+j for i,j in zip(first_names,last_names)]
print(full_names)

# ```{admonition} Challenge Problem (worth Extra Credit)
# I've created new lists that contain strings of the names in the format Lastname,Firstname, with random leading/trailing spaces and terrible capitalizations. Use a list comprehension to make our nice, "Firstname Lastname" list again.
# ```

# In[129]:

all_names = ['sMitH,BoB   ', '  bee,samanthA',' oLIVER,JOHN ','  caRPENTer,reneE  ']

# solution
full_names = [i.strip().split(',')[1].upper()[0]
              +i.strip().split(',')[1].lower()[1:]
              +' '
              +i.strip().split(',')[0].upper()[0]
              +i.strip().split(',')[0].lower()[1:]
              for i in all_names]
print(full_names)

# ```{note}
# Note that with this last example, we're entering a degree of single-line length and complexity that it almost doesn't make sense to use a comprehension anymore. Just because something CAN be done in one line doesn't mean is has to be, or should be.
#
# ```
#
# You may be wondering what use case this type of coding has in astronomy -- turns out, quite a lot. Take this example: you read in a data table and the columns have names like "FLUX HA", "FLUX ERR", etc.
#
# If you're trying to make a `pandas DataFrame` of this table, it is advantageous to rename these columns something like `flux_ha` and `flux_err`. This way, commands like `df.flux_ha` can be used.
#
# Being able to iterate over the string list of column names and turn caps into lower case, spaces into underscores, etc., is a useful skill that will come in handy when wrangling data.
#
# Below, for reference, I show how I myself would do the above example in production code:

# In[133]:

def clean_csv_string(str_in,sep=',',formatting='LastFirst'):
    str_stripped = str_in.strip() # remove trailing/leading spaces
    str_split = str_stripped.split(sep) #split at delimiter
    str_cap_correct = [i.upper()[0]+i.lower()[1:] for i in str_split]
    if formatting=='LastFirst':
        out_string = str_cap_correct[1] + ' ' + str_cap_correct[0]
        return out_string
    elif formmatting=='FirstLast':
        out_string = str_cap_correct[0] + ' ' + str_cap_correct[1]
        return out_string
    else:
        raise ValueError('formatting not set correctly')

full_names = [clean_csv_string(i) for i in all_names]
print(full_names)

# By making the string cleaning steps a function, I could take the time to explain what is going on within the function, as well as control for additional possibilities (like the names being in First,Last formatting). I could make this function more robust and complex, and my final comprehension stays readable and simple, as I loop over the names and run each through my handy functions (with some settings tweaked, potentially).

# ## Question 9
#
# Take the arrays `XX`, `YY`, and `ZZ` below and create one multidimensional array in which they are the columns. Print to confirm this worked.

# In[131]:

XX = np.array([1,2,3,4,5,6,7,8,9])
YY = np.array([5,6,7,8,9,10,11,12,13])
ZZ = np.array([10,11,12,13,14,15,16,17,18])

#solution
cols = np.column_stack((XX,YY,ZZ))
cols

# ## Question 10
#
# Units, units, units. The bane of every scientists' existence... except theorists that set every constant equal to 1.
#
# In the real world, we measure fluxes or magnitudes in astronomical images, infer temperatures and densities from data and simulations, and ultimately have to deal with units one way or another.
#
# Thankfully, our friends at `astropy` know this, and they've come to save the day. This next question serves as an introduction to the `units` module in astropy, which can be both a live saver and a pain in the ass, but at the end of the day is absolutely worth learning.

# In[137]:

import astropy.units as u

# The standard import for this library is `u`, so be careful not to name any variables that letter.
#
# To "assign" units to a variable, we multiply by the desired unit as follows. Note that generally the module knows several aliases/common abrreviations for a unit, if it is uniquely identifiable.

# In[138]:

star_temp = 5000*u.K
star_radius = 0.89 * u.Rsun
star_mass = 0.6 * u.Msun

# We can perform trivial conversions using the `.to()` method.

# In[139]:

star_radius.to(u.km)

# Once we attach units to something, it is now a `Quantity` object. Quantity objects are great, above, we saw they have built-in methods to facilitate conversion. They can also be annoying -- sometimes another function we've written needs just the raw value or array back out. To get this, we use the `.value` attribute of a quantity object:

# In[140]:

star_mass.to(u.kg).value

# This now strips away all `Quantity` stuff and gives us an array or value to use elsewhere in our code.
#
# Units are great because they help us combine quantities while tracking units and dimensional analysis. A common operation in astronomy is converting a flux to a luminosity given a distance, using
#
# $$
# F = \frac{L}{4\pi D^2}
# $$
# where $L$ is the luminosity and $D$ is the distance to the source.
#
# What if I've made a flux measurement in astronomical units such as erg/s/cm$^2$, and I want to know the luminosity in solar luminosities, and my distance happens to be in Mpc? Regardless of my input units, I can easily do this:

# In[141]:

L = 4 * np.pi * (3.6*u.Mpc)**2 * (7.5e-14 * u.erg/u.s/u.cm**2)
L.to(u.Lsun)

# This conversion worked because the units worked out. If my units of flux weren't correct, I'd get an error:

# In[142]:

L = 4 * np.pi * (3.6*u.Mpc)**2 * (7.5e-14 * u.erg/u.s/u.cm**2/u.AA)
L.to(u.Lsun)

# Here, `units` realized that I was putting in units of flux density, but wanted a luminosity out, and ultimately those units don't resolve out. Thus, it can be a great way to catch errors in your inputs.
#
# Note: just be careful that sometimes, you throw a constant into an equation but the constant has some units. If you're going to use the unit module to do a calculation, ALL inputs that HAVE units must be assigned them correctly as above for it to work.

# For your exercise, consider the following:
#
# The virial temperature of a galaxy halo is given roughly by
#
# $$
# T_{\rm vir} \simeq 5.6\times10^4\;\textrm{K}\left(\frac{\mu}{0.59}\right)\left(\frac{M_{\rm halo}}{10^{10}\; M_{\odot}}\right)^{2/3}\left(\frac{1+z}{4}\right)
# $$

# where here, we can assume $\mu$ is 0.59.
#
# Write a function that takes as an input a halo mass, redshift, and optionally $\mu$ (default 0.59), and returns the virial temperature in Kelvin. Your function should take in an astropy quantity with mass units, but should allow for the mass to be input with any appropriate units.

# In[144]:

# Solution

def Tvir(M,z,mu=0.59):
    return ((5.6e4*u.K)*(mu/0.59)*(M/(1e10*u.Msun))**(2/3.)*((1+z)/4)).to(u.K)

Tvir(10**11*u.Msun,z=3)

import numpy,import scipy,import astropy


## python src/chapter2/chapter2.py
## python3 src/chapter2/chapter2.py

import sys
import numpy as nm
from numpy import arange
import matplotlib as mat
import matplotlib.pyplot as plt

class Chapter2:
    '''
    CLRS 第二章 2.1 2.2 算法函数和笔记
    '''
    def __init__(self, ok = 1, *args, **kwargs):
        '''
        Summary
        =
        These are notes of Peefy CLRS chapter1

        Parameters
        =
        *args : a tuple like
        **kwargs : a dict like

        Returns
        =
        self

        Example
        =
        >>> chapter2 = Chapter2(ok = 1);
        '''
        self.ok = ok

    def __hello():
        pass

    def insertSortAscending(self, array = []):
        '''
        Summary
        =
        插入排序的升序排列

        Parameter
        =
        array : a list like
        Return
        =
        sortedArray : 排序好的数组
        >>> array = [1, 3, 5, 2, 4, 6]
        >>> Chapter2().insertSortAscending(array)
        >>> [1, 2, 3, 4, 5, 6]
        '''
        A = array
        n = len(A)
        for j in range(1, n):
            ## Insert A[j] into the sorted sequece A[1...j-1] 前n - 1 张牌
            # 下标j指示了待插入到手中的当前牌，所以j的索引从数组的第二个元素开始
            # 后来摸的牌
            key = A[j]
            # 之前手中的已经排序好的牌的最大索引
            i = j - 1
            # 开始寻找插入的位置并且移动牌
            while(i >= 0 and A[i] > key):
                # 向右移动牌
                A[i + 1] = A[i]
                # 遍历之前的牌
                i = i - 1
            # 后来摸的牌插入相应的位置
            A[i + 1] = key
        # 输出升序排序后的牌
        return A

    def insertSortDescending(self, array = []):
        '''
        Summary
        =
        插入排序的降序排列

        Parameter
        =
        array : a list like

        Return
        =
        sortedArray : 排序好的数组
        >>> array = [1, 3, 5, 2, 4, 6]
        >>> Chapter2().insertSortAscending(array)
        >>> [6, 5, 4, 3, 2, 1]
        '''
        A = array
        n = len(A)
        for j in range(1, n):
            ## Insert A[j] into the sorted sequece A[1...j-1] 前n - 1 张牌
            # 下标j指示了待插入到手中的当前牌，所以j的索引从数组的第二个元素开始
            # 后来摸的牌
            key = A[j]
            # 之前手中的已经排序好的牌的最大索引
            i = j - 1
            # 开始寻找插入的位置并且移动牌
            while(i >= 0 and A[i] < key):
                # 向右移动牌
                A[i + 1] = A[i]
                # 遍历之前的牌
                i = i - 1
            # 后来摸的牌插入相应的位置
            A[i + 1] = key
        # 输出降序排序后的牌
        return A

    def arrayContains(self, array = [], v = None):
        '''
        Summary
        =
        * a function
        * *检测一个数组中是否包含一个元素*

        Parameter
        =
        *array* : a list like
        v : a element

        Return
        =
        index:若找到返回找到的索引，没找到返回None

        Example:
        =
        >>> array = [12, 23, 34, 45]
        >>> v = 23
        >>> m = 55
        >>> Chapter2().arrayContains(array, v)
        >>> 1
        >>> Chapter2().arrayContains(array, m)
        >>> None
        '''
        index = None
        length = len(array)
        for i in range(length):
            if v == array[i]:
                index = i
        return index

    def twoNBinNumAdd(self, A = [], B = []):
        '''
        Summary
        =
        两个存放数组A和B中的n位二进制整数相加

        Parameter
        ====
        A : a list like and element of the list must be 0 or 1
        B : a list like and element of the list must be 0 or 1

        Return
        ======
        returnSum : sum of two numbers

        Example:
        =
        >>> A = [1, 1, 0, 0]
        >>> B = [1, 0, 0, 1]
        >>> Chapter2().twoNBinNumAdd(A, B)
        >>> [1, 0, 1, 0, 1]
        '''
        if len(A) != len(B):
            raise Exception('length of A must be equal to length of B')
        length = len(A)
        # 注意：range 函数和 arange 函数都是 左闭右开区间
        '''
        >>> range(0,3)
        >>> [0, 1, 2]
        '''
        returnSum = arange(length + 1)
        bitC = 0
        for i in range(length):
            index = length - 1 - i
            bitSum = A[index] + B[index] + bitC
            if bitSum >= 2:
                bitSum = 0
                bitC = 1
            else:
                bitC = 0
            returnSum[index + 1] = bitSum
            if index == 0:
                returnSum[0] = bitC
        return returnSum

    def selectSortAscending(self, array = []):
        '''
        Summary
        =
        选择排序的升序排列

        Parameter
        =
        array : a list like
        Return
        =
        sortedArray : 排序好的数组
        >>> array = [1, 3, 5, 2, 4, 6]
        >>> Chapter2().selectSortAscending(array)
        >>> [1, 2, 3, 4, 5, 6]
        '''
        A = array
        length = len(A)
        for j in range(length):
            minIndex = j
            # 找出A中第j个到最后一个元素中的最小值
            # 仅需要在头n-1个元素上运行
            for i in range(j, length):
                if A[i] <= A[minIndex]:
                    minIndex = i
            # 最小元素和最前面的元素交换
            min = A[minIndex]
            A[minIndex] = A[j]
            A[j] = min
        return A

    def note(self, *args, **kwargs):
        '''
        Summary
        =
        These are notes of Peefy CLRS chapter1

        Parameters
        =
        *args : a tuple like
        **kwargs : a dict like

        Returns
        =
        self

        Example
        =
        >>> Chapter2().note()
        '''
        print('排序算法有很多，包括插入排序，冒泡排序，堆排序，归并排序，选择排序，计数排序，基数排序，桶排序，快速排序')
        print('2.1 插入排序')
        print('插入排序(INSERTION-SORT):输入n个数，输出n个数的升序或者降序排列')
        print('插入排序是一个对少量元素进行排序的有效算法，工作做原理与打牌摸牌整理手中的牌差不多')
        print('以下是Python的插入排序(升序)算法(模拟打牌)')
        print('书中的伪代码数组索引从1开始，python数组索引从0开始')
        A = [4, 4.5, 2, 5, 1.2, 3.5]
        print("待排序的序列：", A)
        print("插入排序后的序列：", self.insertSortAscending(A))
        print('循环不变式主要用来帮助理解插入算法的正确性。证明循环不变式的三个性质')
        print(' 1.初始化：在循环的第一轮迭代开始前，应该是正确的')
        print(' 2.保持：如果在循环的某一次迭代开始之前它是正确的，那么在下一次迭代开始前，它也应该保持正确')
        print(' 3.终止：当循环结束时，不变式给了我们一个有用的性质，有助于表明算法是正确的')
        print('数学归纳法中，要证明某一性质是成立的，必须首先证明其基本情况和一个归纳步骤都是成立的')
        print('插入排序的循环不变式证明：')
        print(' 1.初始化：插入排序第一步首相将数组中第二个元素当做待插入的元素，被插入的元素只有数组中第一元素，显然一个元素是已经排序好的')
        print(' 2.保持：证明每一轮循环都能时循环不变式保持成立,同时证明外层for循环和内层while循环同时满足循环不变式')
        print(' 3.终止：当j大于n时，外层循环结束，新的数组包含了原来数组中的元素，并且是排序好的，算法正确')
        print('布尔运算符and和or都具有短路运算能力')
        print('练习2.1-1：对于序列[31, 41, 59, 26, 41, 58]首先选出序列中第二个元素41向前插入(升序)，接下来选出59向前插入，依次类推')
        print('练习2.1-2：只要把书中插入排序中的伪代码的不等号方向更换即可')
        A = [31, 41, 59, 26, 21, 58]
        print('  排序好的降序序列为：', self.insertSortDescending(A))
        print('练习2.1-3：结果如下:')
        print('  32在序列A中的索引为(索引从0开始)：', self.arrayContains(A, 32))
        print('  21在序列A中的索引为(索引从0开始)：', self.arrayContains(A, 21))
        print('练习2.1-4：两个n位二进制数相加的算法(适用于FPGA中。参考一位加法器的逻辑表达式或者数学表达式)：')
        print(' 两个n位二进制数的和为：', self.twoNBinNumAdd([1, 1, 0, 1], [1, 0, 0, 0]))
        # range函数和np.arange函数都是左闭右开区间
        print('range(0,4)的值为：', [i for i in range(0,4)])
        print('2.2 算法分析')
        print('算法分析即指对一个算法所需要的资源进行预测')
        print('内存，通信带宽或者计算机硬件等资源是关心的资源, 通常资源指我们希望测度的计算时间')
        print('采用单处理器、随机存取机RAM计算模型')
        print('RAM模型包含了真实计算机中常见的指令：算数指令(加法，减法，除法，取余，向下取整，向上取整指令)，数据移动指令(装入、存储、复制)和控制指令(条件和非条件转移、子程序调用和返回指令)')
        print('RAM模型中的数据类型有整数类型和浮点实数类型')
        print('算法分析所需要的数学工具包括组合数学、概率论、代数')
        print('需要对"运行时间"和"输入规模"更仔细地加以定义')
        print('插入排序算法的分析')
        print('插入排序INSERTION=SORT过程的时间开销与输入有关，排序1000个数的事件比排序三个数的时间要长')
        print('插入算法即使对给定规模的输入，运行时间也有可能依赖于给定的是该规模下的哪种输入')
        print('插入排序当输入是最好情况(即输入的序列已经按顺序排好)，插入排序所需要的时间随输入规模是线性的O(n)')
        print('插入排序当输入是按照逆序排序的(降序排列输入后输出升序排列),就会出现最坏情况,所需要时间是输入规模的二次函数O(n^2)')
        print('一般考察算法的最坏情况运行时间')
        print('当然对于一些"随机化"算法，其行为即使对于固定的输入，运行时间也是可以变化的')
        print('做进一步的抽象：即运行时间的增长率，只考虑算法运行时间公式中的最高次项，并且忽略最高次项的常数系数,时间复杂度')
        print('练习题2.2-1: n^3/1000 - 100n^2 - 100n + 3 的时间复杂度：O(n^3)')
        print('练习题2.2-2:选择排序如下：')
        A = [21, 11, 9, 66, 51, 48]
        print(' 选择排序排列前的元素：', A)
        print(' 选择排序排列后的元素：', self.selectSortAscending(A))
        print(' 选择排序最好情况o(n^2),最坏情况o(n^2)')
        print(' 因为在第n-1次比较选择的时候已经比较出了最大元素和次大元素并选择，所以这时选择完之后第n个元素已经是最大值，没有必要再比较下去了')
        print('练习题2.2-3:线性查找最好情况是o(1),最坏情况是o(n)，平均情况是(n)')
        print('要使算法具有较好的最佳情况运行时间就一定要对输入进行控制，使之偏向能够使得算法具有最佳运行情况的排列。')

        #python src/chapter2/chapter2.py
        #python3 src/chapter2/chapter2.py
        return self

if __name__ == '__main__':
    print('Run main : single chapter two!')
    Chapter2().note()
else:
    pass

## python src/chapter2/chapter2.py
## python3 src/chapter2/chapter2.py


#!/usr/bin/env python
# coding: utf-8

# # ETHZ: 227-0966-00L
# # Quantitative Big Imaging
# # March 14, 2019
#
# ## Basic Segmentation and Discrete Binary Structures
# ### Part 1

# # Lesson Outline
# - Motivation
# - Qualitative Approaches
# - Thresholding
#  - Other types of images
#  - Selecting a good threshold
# - Implementation
# - Morphology
# - Contouring / Mask Creation

# ### Applications
#
# - Simple two-phase materials (bone, cells, etc)
#   - Beyond 1 channel of depth
# - Multiple phase materials
# - Filling holes in materials
# - Segmenting Fossils
# - Attempting to segment the cortex in brain imaging
# ![Cortex Image](ext-figures/cortex.png)

# # Literature / Useful References
#
# - John C. Russ, “The Image Processing Handbook”,(Boca Raton, CRC Press)
#  - Available [online](http://dx.doi.org/10.1201/9780203881095) within domain ethz.ch (or proxy.ethz.ch / public VPN)
#
# ### Models / ROC Curves
#
# - [Julia Evans - Recalling with Precision](https://www.youtube.com/watch?v=ryZL4XNUmwo)
# - [Stripe's Next Top Model](https://github.com/stripe/topmodel)

# # Motivation:  Why do we do imaging experiments?
#
#
# - Exploratory
#  - To visually, qualitatively examine samples and differences between them
#  - No prior knowledge or expectations
# - To test a hypothesis
#  - Quantitative assessment coupled with statistical analysis
#  - Does temperature affect bubble size?
#  - Is this gene important for cell shape and thus mechanosensation in bone?
#  - Does higher canal volume make bones weaker?
#  - Does the granule shape affect battery life expectancy?
#

# - What we are looking at
# ![Standard Cell, http://en.wikipedia.org/wiki/File:Average_prokaryote_cell-_en.svg](ext-figures/Average_prokaryote_cell.svg)
# - What we get from the imaging modality

# In[1]:

from skimage.io import imread
from skimage.color import rgb2gray
import matplotlib.pyplot as plt

dkimg = imread("../common/figures/Average_prokaryote_cell.jpg")
plt.matshow(rgb2gray(dkimg), cmap="bone")

# # To test a hypothesis
# - We perform an experiment bone to see how big the cells are inside the tissue
# $$\downarrow$$ ![Bone Measurement](ext-figures/tomoimage.png)
#
# ### 2560 x 2560 x 2160 x 32 bit = 56GB / sample
# - Filtering and Preprocessing!
# $$\downarrow$$
# - 20h of computer time later ...
# - 56GB of less noisy data
# - Way too much data, we need to reduce
#

# # What did we want in the first place
#
#
# ### _Single number_:
# * volume fraction,
# * cell count,
# * average cell stretch,
# * cell volume variability

# # Why do we perform segmentation?
#
# - In model-based analysis every step we peform, simple or complicated is related to an underlying model of the system we are dealing with
# - [_Occam's Razor_](http://en.wikipedia.org/wiki/Occams_Razor) is very important here : The simplest solution is usually the right one
#  - Bayesian, neural networks optimized using genetic algorithms with Fuzzy logic has a much larger parameter space to explore, establish sensitivity in, and must perform much better and be tested much more thoroughly than thresholding to be justified.
#  - We will cover some of these techniques in the next 2 lectures since they can be very powerful particularly with unknown data

# # Review: Filtering and Image Enhancement
#
#
# - This was a noise process which was added to otherwise clean imaging data
# - $$ I_{measured}(x,y) = I_{sample}(x,y) + \text{Noise}(x,y) $$
# - What would the perfect filter be
#  - $$ \textit{Filter} \ast I_{sample}(x,y) = I_{sample}(x,y) $$
#  - $$ \textit{Filter} \ast \text{Noise}(x,y) = 0 $$
#  - $$ \textit{Filter} \ast I_{measured}(x,y) = \textit{Filter} \ast I_{real}(x,y) + \textit{Filter}\ast \text{Noise}(x,y) \rightarrow \bf I_{sample}(x,y) $$
# - What most filters end up doing
# $$ \textit{Filter} \ast I_{measured}(x,y) = 90\%  I_{real}(x,y) + 10\% \text{Noise}(x,y) $$
# - What bad filters do
# $$ \textit{Filter} \ast I_{measured}(x,y) = 10\% I_{real}(x,y) + 90\% \text{Noise}(x,y) $$

# # Qualitative Metrics: What did people used to do?
#
# - What comes out of our detector / enhancement process

# In[2]:

from skimage.io import imread
from skimage.color import rgb2gray
import matplotlib.pyplot as plt

dkimg = rgb2gray(imread("../common/figures/Average_prokaryote_cell.jpg"))
fig, (ax_hist, ax_img) = plt.subplots(1, 2, figsize=(12, 6))

ax_hist.hist(dkimg.ravel())
ax_hist.set_xlabel("Absorption Coefficient")
ax_hist.set_ylabel("Pixel Count")

m_show_obj = ax_img.matshow(dkimg, cmap="bone")
cb_obj = plt.colorbar(m_show_obj)
cb_obj.set_label("Absorption Coefficient")

# - Identify objects by eye
#  - Count, describe qualitatively: "many little cilia on surface", "long curly flaggelum", "elongated nuclear structure"
# - Morphometrics
#  - Trace the outline of the object (or sub-structures)
#  - Can calculate the area by using equal-weight-paper
#  - Employing the "[cut-and-weigh](http://ion.chem.usu.edu/~sbialkow/Classes/361/GC/GC.html)" method
#

# # Segmentation Approaches
#
#
# They match up well to the world view / perspective
#
# ![Approaches](../common/figures/approaches.png)

# ### Model-Based
#
# - $\rightarrow$ Experimentalist
# - Problem-driven
#  - Top-down
#  - _Reality_ Model-based
#
#
# ### Machine Learning Approach
#
# - $\rightarrow$ Computer Vision / Deep Learning
# - Results-driven

# # Model-based Analysis
#
# ![Traditional Imaging](../common/figures/image-formation.png)
#
# - Many different imaging modalities ( $\mu \textrm{CT}$ to MRI to Confocal to Light-field to AFM).
# - Similarities in underlying equations
#  - different coefficients, units, and mechanism
#
# $$ I_{measured}(\vec{x}) = F_{system}(I_{stimulus}(\vec{x}),S_{sample}(\vec{x})) $$

# # Direct Imaging (simple)
#
# In many setups there is un-even illumination caused by incorrectly adjusted equipment and fluctations in power and setups
#
# - $F_{system}(a,b) = a*b$
# - $I_{stimulus} = \textrm{Beam}_{profile}$
# - $S_{system} = \alpha(\vec{x})$
#
# $\longrightarrow \alpha(\vec{x})=\frac{I_{measured}(\vec{x})}{\textrm{Beam}_{profile}(\vec{x})}$
#
#

# In[3]:

from skimage.io import imread
from skimage.color import rgb2gray
import matplotlib.pyplot as plt
from skimage.morphology import disk
from scipy.ndimage import zoom
import numpy as np

cell_img = 1 - rgb2gray(imread("../common/figures/Average_prokaryote_cell.jpg"))
s_beam_img = np.pad(
    disk(2) / 1.0, [[1, 1], [1, 1]], mode="constant", constant_values=0.2
)
beam_img = zoom(s_beam_img, [cell_img.shape[0] / 7.0, cell_img.shape[1] / 7.0])

fig, (ax_beam, ax_img, ax_det) = plt.subplots(1, 3, figsize=(12, 4))

ax_beam.imshow(beam_img, cmap="hot")
ax_beam.set_title("Beam Profile")

ax_img.imshow(cell_img, cmap="hot")
ax_img.set_title("Sample Profile")

ax_det.imshow(cell_img * beam_img, cmap="hot")
ax_det.set_title("Detector")

# In[4]:

fig, (ax_prof) = plt.subplots(1, 1, figsize=(12, 4))

ax_prof.plot(beam_img[beam_img.shape[0] // 2], label="Beam Profile")
ax_prof.plot(cell_img[beam_img.shape[0] // 2], label="Sample Image")
ax_prof.plot((cell_img * beam_img)[beam_img.shape[0] // 2], label="Detector")
ax_prof.set_ylabel("Intensity")
ax_prof.set_xlabel("Pixel Position")
# make an interactive plot
import plotly.offline as py
import plotly.tools as tls

py.init_notebook_mode()
py.iplot(tls.mpl_to_plotly(fig))

# Frequently there is a fall-off of the beam away from the center (as is the case of a Gaussian beam which frequently shows up for laser systems). This can make extracting detail away from the center challenging

# In[5]:

fig, ax1 = plt.subplots(1, 1, figsize=(8, 8))
ax1.matshow(cell_img * beam_img, cmap="hot")

# # Absorption Imaging (X-ray, Ultrasound, Optical)
#
# - For absorption/attenuation imaging $\rightarrow$ [Beer-Lambert Law](http://en.wikipedia.org/wiki/Attenuation_coefficient)
#  $$ I_{detector} = \underbrace{I_{source}}_{I_{stimulus}}\underbrace{\exp(-\alpha d)}_{S_{sample}} $$
#  - Different components have a different $\alpha$ based on the strength of the interaction between the light and the chemical / nuclear structure of the material
# $$ I_{sample}(x,y) = I_{source}\exp(-\alpha(x,y) d) $$
# $$ \alpha = f(N,Z,\sigma,\cdots) $$
#
# - For segmentation this model is:
#  - there are 2 (or more) distinct components that make up the image
#  - these components are distinguishable by their values (or vectors, colors, tensors, ...)
#

# In[6]:

import matplotlib.pyplot as plt
import numpy as np
import pandas as pd

I_source = 1.0
d = 1.0
alpha_1 = np.random.normal(1, 0.25, size=100)
alpha_2 = np.random.normal(2, 0.25, size=100)
alpha_3 = np.random.normal(3, 0.5, size=100)

abs_df = pd.DataFrame(
    [
        dict(alpha=c_x, material=c_mat)
        for c_vec, c_mat in zip(
            [alpha_1, alpha_2, alpha_3], ["material 1", "material 2", "material 3"]
        )
        for c_x in c_vec
    ]
)
abs_df["I_detector"] = I_source * np.exp(-abs_df["alpha"] * d)

fig, ((ax1, ax2), (ax3, ax4)) = plt.subplots(2, 2, figsize=(12, 8))
for c_mat, c_df in abs_df.groupby("material"):
    ax1.scatter(x=c_df["alpha"], y=c_df["I_detector"], label=c_mat)
    ax3.hist(c_df["alpha"], alpha=0.5, label=c_mat)
    ax2.hist(c_df["I_detector"], alpha=0.5, label=c_mat, orientation="horizontal")
ax1.set_xlabel("$\\alpha(x,y)$", fontsize=15)
ax1.set_ylabel("$I_{detector}$", fontsize=18)
ax1.legend()
ax2.legend()
ax3.legend(loc=0)

ax4.axis("off")

# # Example Mammography
# Mammographic imaging is an area where model-based absorption imaging is problematic. Even if we assume a constant illumination (_rarely_ the case),
#
# $$ I_{detector} = \underbrace{I_{source}}_{I_{stimulus}}\underbrace{\exp(-\alpha d)}_{S_{sample}} $$
# $$ \downarrow $$
# $$ I_{detector} = \exp(-\alpha(x,y) d(x,y)) $$
# $$ \downarrow $$
# $$ I_{detector} = \exp(-\int_{0}^{l}\alpha(x,y, z) dz) $$
#

# Specifically the problem is related to the inability to separate the $\alpha$ and $d$ terms. We model a basic breast volume as a half sphere with a constant absorption factor.
#
# $$\alpha(x,y,z) = 1e-2$$
#
# The $\int$ then turns into a $\Sigma$ in discrete space

# In[7]:

import matplotlib.pyplot as plt
import numpy as np
from skimage.morphology import ball

breast_mask = ball(50)[:, 50:]

# just for 3d rendering, don't worry about it
import plotly.offline as py
from plotly.figure_factory import create_trisurf

py.init_notebook_mode()
from skimage.measure import marching_cubes_lewiner

vertices, simplices, _, _ = marching_cubes_lewiner(breast_mask > 0)
x, y, z = zip(*vertices)
fig = create_trisurf(
    x=x, y=y, z=z, plot_edges=False, simplices=simplices, title="Breast Phantom"
)
py.iplot(fig)

# In[8]:

breast_alpha = 1e-2
breast_vol = breast_alpha * breast_mask
i_detector = np.exp(-np.sum(breast_vol, 2))

fig, (ax_hist, ax_breast) = plt.subplots(1, 2, figsize=(20, 8))

b_img_obj = ax_breast.imshow(i_detector, cmap="bone_r")
plt.colorbar(b_img_obj)

ax_hist.hist(i_detector.flatten())
ax_hist.set_xlabel("$I_{detector}$")
ax_hist.set_ylabel("Pixel Count")

# If we know that $\alpha$ is constant we can reconstruct d from the image

# In[9]:

breast_thickness = -np.log(i_detector) / breast_alpha
fig, (ax_hist, ax_breast) = plt.subplots(1, 2, figsize=(12, 5))

b_img_obj = ax_breast.imshow(breast_thickness, cmap="bone")
plt.colorbar(b_img_obj)

ax_hist.hist(breast_thickness.flatten())
ax_hist.set_xlabel("Breast Thickness ($d$)\nIn Pixels")
ax_hist.set_ylabel("Pixel Count")

# In[10]:

from mpl_toolkits.mplot3d import Axes3D

fig = plt.figure(figsize=(8, 4), dpi=200)
ax = fig.gca(projection="3d")
# Plot the surface.
yy, xx = np.meshgrid(
    np.linspace(0, 1, breast_thickness.shape[1]),
    np.linspace(0, 1, breast_thickness.shape[0]),
)
surf = ax.plot_surface(
    xx, yy, breast_thickness, cmap=plt.cm.jet, linewidth=0, antialiased=False
)
ax.view_init(elev=30, azim=45)
ax.set_zlabel("Breast Thickness")

# We run into problems when the $\alpha$ is no longer constant. For example if we place a dark lump in the center of the breast. It is impossible to tell if the breast if thicker or if the lump inside is denser. For the lump below we can see on the individual slices of the sample that the lesion appears quite clearly and is very strangely shaped.

# In[11]:

breast_vol = breast_alpha * breast_mask
renorm_slice = np.sum(breast_mask[10:40, 0:25], 2) / np.sum(breast_mask[30, 10])
breast_vol[10:40, 0:25] /= np.stack([renorm_slice] * breast_vol.shape[2], -1)

from skimage.util import montage as montage2d

fig, ax1 = plt.subplots(1, 1, figsize=(12, 12))
ax1.imshow(
    montage2d(breast_vol.swapaxes(0, 2).swapaxes(1, 2)[::3]),
    cmap="bone",
    vmin=breast_alpha * 0.8,
    vmax=breast_alpha * 1.2,
)

# When we make the projection and apply Beer's Law we see that it appears as a relatively constant region in the image

# In[12]:

i_detector = np.exp(-np.sum(breast_vol, 2))

fig, (ax_hist, ax_breast) = plt.subplots(1, 2, figsize=(20, 8))

b_img_obj = ax_breast.imshow(i_detector, cmap="bone_r")
plt.colorbar(b_img_obj)

ax_hist.hist(i_detector.flatten())
ax_hist.set_xlabel("$I_{detector}$")
ax_hist.set_ylabel("Pixel Count")

# And as a flat constant region in the thickness reconstruction. So we fundamentally from this single image cannot answer is the breast oddly shaped or does it have an possible tumor inside of it

# In[13]:

breast_thickness = -np.log(i_detector) / 1e-2
fig, (ax_hist, ax_breast) = plt.subplots(1, 2, figsize=(12, 5))

b_img_obj = ax_breast.imshow(breast_thickness, cmap="bone")
plt.colorbar(b_img_obj)

ax_hist.hist(breast_thickness.flatten())
ax_hist.set_xlabel("Breast Thickness ($d$)\nIn Pixels")
ax_hist.set_ylabel("Pixel Count")

# In[14]:

from mpl_toolkits.mplot3d import Axes3D

fig = plt.figure(figsize=(8, 4), dpi=200)
ax = fig.gca(projection="3d")
# Plot the surface.
yy, xx = np.meshgrid(
    np.linspace(0, 1, breast_thickness.shape[1]),
    np.linspace(0, 1, breast_thickness.shape[0]),
)
surf = ax.plot_surface(
    xx, yy, breast_thickness, cmap=plt.cm.jet, linewidth=0, antialiased=False
)
ax.view_init(elev=30, azim=130)
ax.set_zlabel("Breast Thickness")

# # Where does segmentation get us?
#
#
# - We convert a decimal value (or something even more complicated like 3 values for RGB images, a spectrum for hyperspectral imaging, or a vector / tensor in a mechanical stress field)
# - to a single, discrete value (usually true or false, but for images with phases it would be each phase, e.g. bone, air, cellular tissue)
#
# - __2560 x 2560 x 2160 x 32 bit = 56GB / sample__
# $$\downarrow$$
# - 2560 x 2560 x 2160 x **1 bit** = 1.75GB / sample
#

# # Applying a threshold to an image
# Start out with a simple image of a cross with added noise
# $$ I(x,y) = f(x,y) $$

# In[15]:

import matplotlib.pyplot as plt
import numpy as np
import pandas as pd

nx = 5
ny = 5
xx, yy = np.meshgrid(
    np.arange(-nx, nx + 1) / nx * 2 * np.pi, np.arange(-ny, ny + 1) / ny * 2 * np.pi
)
cross_im = 1.5 * np.abs(np.cos(xx * yy)) / (
    np.abs(xx * yy) + (3 * np.pi / nx)
) + np.random.uniform(-0.25, 0.25, size=xx.shape)
plt.matshow(cross_im, cmap="hot")
plt.colorbar()

# The intensity can be described with a probability density function
# $$ P_f(x,y) $$

# In[16]:

fig, ax1 = plt.subplots(1, 1)
ax1.hist(cross_im.ravel(), 20)
ax1.set_title("$P_f(x,y)$")
ax1.set_xlabel("Intensity")
ax1.set_ylabel("Pixel Count")

# # Applying a threshold to an image
#
# By examining the image and probability distribution function, we can _deduce_ that the underyling model is a whitish phase that makes up the cross and the darkish background
#
# Applying the threshold is a deceptively simple operation
#
# $$ I(x,y) =
# \begin{cases}
# 1, & f(x,y)\geq0.40 \\
# 0, & f(x,y)<0.40
# \end{cases}$$

# In[17]:

fig, ax1 = plt.subplots(1, 1)
ax1.imshow(cross_im, cmap="hot", extent=[xx.min(), xx.max(), yy.min(), yy.max()])
thresh_img = cross_im > 0.4

ax1.plot(
    xx[np.where(thresh_img)],
    yy[np.where(thresh_img)],
    "ks",
    markerfacecolor="green",
    alpha=0.5,
    label="threshold",
    markersize=20,
)
ax1.legend()

# # Various Thresholds
# We can see the effect of choosing various thresholds
#

# In[18]:

fig, m_axs = plt.subplots(3, 3, figsize=(15, 15))
for c_thresh, ax1 in zip(np.linspace(0.1, 0.9, 9), m_axs.flatten()):

    ax1.imshow(cross_im, cmap="bone", extent=[xx.min(), xx.max(), yy.min(), yy.max()])
    thresh_img = cross_im > c_thresh

    ax1.plot(
        xx[np.where(thresh_img)],
        yy[np.where(thresh_img)],
        "rs",
        alpha=0.5,
        label="img>%2.2f" % c_thresh,
        markersize=20,
    )
    ax1.legend(loc=1)

# # Segmenting Cells
#
# - We can peform the same sort of analysis with this image of cells
# - This time we can derive the model from the basic physics of the system
#  - The field is illuminated by white light of nearly uniform brightness
#  - Cells absorb light causing darker regions to appear in the image
#  - _Lighter_ regions have no cells
#  - __Darker__ regions have cells

# In[19]:

from skimage.io import imread
from skimage.color import rgb2gray
import matplotlib.pyplot as plt
import numpy as np

cell_img = rgb2gray(imread("../common/figures/Cell_Colony.jpg"))
fig, (ax_hist, ax_img) = plt.subplots(1, 2, figsize=(12, 6))
ax_hist.hist(cell_img.ravel(), np.arange(255))
ax_obj = ax_img.matshow(cell_img, cmap="bone")
plt.colorbar(ax_obj)

# In[20]:

from skimage.color import label2rgb

fig, m_axs = plt.subplots(3, 3, figsize=(15, 15), dpi=200)
for c_thresh, ax1 in zip(np.linspace(100, 200, 9), m_axs.flatten()):
    thresh_img = cell_img < c_thresh

    ax1.imshow(label2rgb(thresh_img, image=1 - cell_img, bg_label=0, alpha=0.4))

    ax1.set_title("img<%2.2f" % c_thresh)

# # Other Image Types
#
# While scalar images are easiest, it is possible for any type of image
# $$ I(x,y) = \vec{f}(x,y) $$

# In[21]:

import pandas as pd
import matplotlib.pyplot as plt
import numpy as np

nx = 10
ny = 10
xx, yy = np.meshgrid(
    np.linspace(-2 * np.pi, 2 * np.pi, nx), np.linspace(-2 * np.pi, 2 * np.pi, ny)
)

intensity_img = 1.5 * np.abs(np.cos(xx * yy)) / (
    np.abs(xx * yy) + (3 * np.pi / nx)
) + np.random.uniform(-0.25, 0.25, size=xx.shape)

base_df = pd.DataFrame(
    dict(x=xx.ravel(), y=yy.ravel(), I_detector=intensity_img.ravel())
)

base_df["x_vec"] = base_df.apply(
    lambda c_row: c_row["x"]
    / np.sqrt(1e-2 + np.square(c_row["x"]) + np.square(c_row["y"])),
    1,
)
base_df["y_vec"] = base_df.apply(
    lambda c_row: c_row["y"]
    / np.sqrt(1e-2 + np.square(c_row["x"]) + np.square(c_row["y"])),
    1,
)

base_df.head(5)

# In[22]:

import seaborn as sns

sns.pairplot(base_df)

# In[23]:

fig, ax1 = plt.subplots(1, 1, figsize=(8, 8))
ax1.quiver(
    base_df["x"],
    base_df["y"],
    base_df["x_vec"],
    base_df["y_vec"],
    base_df["I_detector"],
    cmap="hot",
)

# # Applying a threshold
#
# A threshold is now more difficult to apply since there are now two distinct variables to deal with. The standard approach can be applied to both
# $$ I(x,y) =
# \begin{cases}
# 1, & \vec{f}_x(x,y) \geq0.25 \text{ and}\\
# & \vec{f}_y(x,y) \geq0.25 \\
# 0, & \text{otherwise}
# \end{cases}$$

# In[24]:

thresh_df = base_df.copy()
thresh_df["thresh"] = thresh_df.apply(
    lambda c_row: c_row["x_vec"] > 0.25 and c_row["y_vec"] > 0.25, 1
)

fig, ax1 = plt.subplots(1, 1, figsize=(8, 8))
ax1.quiver(
    thresh_df["x"],
    thresh_df["y"],
    thresh_df["x_vec"],
    thresh_df["y_vec"],
    thresh_df["thresh"],
    cmap="hot",
)

# This can also be shown on the joint probability distribution as

# In[25]:

fig, ax1 = plt.subplots(1, 1, figsize=(4, 4), dpi=200)
ax1.hist2d(thresh_df["x_vec"], thresh_df["y_vec"], cmap="hot")
ax1.set_xlabel("$\\vec{f}_x(x,y)$")
ax1.set_ylabel("$\\vec{f}_y(x,y)$")

# # Applying a threshold
# Given the presence of two variables; however, more advanced approaches can also be investigated. For example we can keep only components parallel to the x axis by using the dot product.
# $$ I(x,y) =
# \begin{cases}
# 1, & |\vec{f}(x,y)\cdot \vec{i}| = 1 \\
# 0, & \text{otherwise}
# \end{cases}$$

# # Looking at Orientations
# We can tune the angular acceptance by using the fact $$\vec{x}\cdot\vec{y}=|\vec{x}| |\vec{y}| \cos(\theta_{x\rightarrow y}) $$
# $$ I(x,y) =
# \begin{cases}
# 1, & \cos^{-1}(\vec{f}(x,y)\cdot \vec{i}) \leq \theta^{\circ} \\
# 0, & \text{otherwise}
# \end{cases}$$


#!/usr/bin/env python
# coding: utf-8

# # 16 - AgriPV - 3-up and 4-up collector optimization
#
#
# This journal helps the exploration of varying collector widths and xgaps in the ground underneath as well as on the rear irradiance for bifacial AgriPV. The optimization varies the numpanels combinations with xgaps for having 3-up and 4-up collectors with varying space along the row (xgap). The actual raytracing is not performed in the jupyter journal but rather on the HPC, but the geometry is the same as presented here.
#
# The steps on this journal:
#

#
  Making Collectors for each number panel and xgap case

#
  Builds the Scene so it can be viewed with rvu

#
#
# An area of 40m x 20 m area is sampled on the HPC, and is highlighted in the visualizations below with an appended terrain of 'litesoil'. The image below shows the two extremes of the variables optimized and the raytrace results, including the worst-case shading experienced under the array ( 100 - min_irradiance *100 / GHI).
#
#
#
# ![AgriPV Collector Width and Xgap Optimization](../images_wiki/AdvancedJournals/AgriPV_CWandXgap_Optimization.PNG)
#

# In[1]:

import os
from pathlib import Path

testfolder = Path().resolve().parent.parent / 'bifacial_radiance' / 'TEMP' /  'Tutorial_16'
if not os.path.exists(testfolder):
    os.makedirs(testfolder)

print ("Your simulation will be stored in %s" % testfolder)

# In[2]:

import bifacial_radiance
import numpy as np

rad_obj = bifacial_radiance.RadianceObj('tutorial_16', str(testfolder))

#

# ## 1. Making Collectors for each number panel and xgap case

# In[3]:

x = 2
y = 1
ygap = 0.1524 # m = 6 in
zgap = 0.002 # m, veyr little gap to torquetube.

tubeParams = {'diameter':0.15,
              'tubetype':'square',
              'material':'Metal_Grey',
              'axisofrotation':True,
               'visible': True}

ft2m = 0.3048
xgaps = [3, 4, 6, 9, 12, 15, 18, 21]
numpanelss = [3, 4]

# Loops
for ii in range(0, len(numpanelss)):
    numpanels = numpanelss[ii]
    for jj in range(0, len(xgaps)):
        xgap = xgaps[jj]*ft2m

        moduletype = 'test-module_'+str(numpanels)+'up_'+str(round(xgap,1))+'xgap'
        rad_obj.makeModule(moduletype,
                    x=x, y=y,
                    xgap=xgap, zgap=zgap, ygap = ygap, numpanels=numpanels,
                    tubeParams=tubeParams)

#

# ## 2. Build the Scene so it can be viewed with rvu

# In[4]:

xgaps = np.round(np.array([3, 4, 6, 9, 12, 15, 18, 21]) * ft2m,1)
numpanelss = [3, 4]
sensorsxs = np.array(list(range(0, 201)))

# Select CASE:
xgap = np.round(xgaps[-1],1)
numpanels = 4

# All the rest

ft2m = 0.3048
hub_height = 8.0 * ft2m
y = 1
pitch = 0.001 # If I recall, it doesn't like when pitch is 0 even if it's a single row, but any value works here.
ygap = 0.15
tilt = 18

sim_name = ('Coffee_'+str(numpanels)+'up_'+
            str(round(xgap,1))+'_xgap')

albedo = 0.35 # Grass value from Torres Molina, "Measuring UHI in Puerto Rico" 18th LACCEI
            # International Multi-Conference for Engineering, Education, and Technology

azimuth = 180
if numpanels == 3:
    nMods = 9
if numpanels == 4:
    nMods = 7
nRows = 1

moduletype = 'test-module_'+str(numpanels)+'up_'+str(round(xgap,1))+'xgap'

rad_obj.setGround(albedo)
lat = 18.202142
lon = -66.759187
metfile = rad_obj.getEPW(lat,lon)
rad_obj.readWeatherFile(metfile)

sceneDict = {'tilt':tilt,'pitch':pitch,'hub_height':hub_height,'azimuth':azimuth, 'nMods': nMods, 'nRows': nRows}
scene = rad_obj.makeScene(module=moduletype,sceneDict=sceneDict,  radname = sim_name)

rad_obj.gendaylit(4020)

octfile = rad_obj.makeOct(filelist = rad_obj.getfilelist(), octname = rad_obj.basename)

name='SampleArea'
text='! genbox litesoil cuteBox 40 20 0.01 | xform -t -20 -10 0.01'
customObject =rad_obj.makeCustomObject(name,text)
rad_obj.appendtoScene(scene.radfiles, customObject, '!xform -rz 0')

octfile = rad_obj.makeOct(rad_obj.getfilelist())

#
# ### To View the generated Scene, you can navigate to the testfolder on a terminal and use:
#
# front view:
# > rvu -vf views\front.vp -e .0265652 -vp 2 -21 2.5 -vd 0 1 0 makemod.oct
#
#  top view:
# > rvu -vf views\front.vp -e .0265652 -vp 5 0 70 -vd 0 0.0001 -1 makemod.oct
#
# ### Or run it directly from Jupyter by removing the comment from the following cell:
#

# In[5]:

## Comment the ! line below to run rvu from the Jupyter notebook instead of your terminal.
## Simulation will stop until you close the rvu window

#!rvu -vf views\front.vp -e .0265652 -vp 2 -21 2.5 -vd 0 1 0 makemod.oct
#!rvu -vf views\front.vp -e .0265652 -vp 5 0 70 -vd 0 0.0001 -1 makemod.oct

docs/tutorials/16 - AgriPV - 3-up and 4-up collector optimization.py