# PHY321: Forces, Newton's Laws and Motion Example

**[Morten Hjorth-Jensen](http://mhjgit.github.io/info/doc/web/)**, Department of Physics and Astronomy and National Superconducting Cyclotron Laboratory, Michigan State University, USA and Department of Physics, University of Oslo, Norway

 **[Scott Pratt](https://pa.msu.edu/profile/pratts/)**, Department of Physics and Astronomy and National Superconducting Cyclotron Laboratory, Michigan State University, USA

 **[Carl Schmidt](https://pa.msu.edu/profile/schmidt/)**, Department of Physics and Astronomy, Michigan State University, USA

Date: **Dec 16, 2020**

Copyright 1999-2020, [Morten Hjorth-Jensen](http://mhjgit.github.io/info/doc/web/). Released under CC Attribution-NonCommercial 4.0 license

# Basic Steps of Scientific Investigations

An overarching aim in this course is to give you a deeper
understanding of the scientific method. The problems we study will all
involve cases where we can apply classical mechanics. In our previous
material we already assumed that we had a model for the motion of an
object.  Alternatively we could have data from experiment (like Usain
Bolt's 100m world record run in 2008).  Or we could have performed
ourselves an experiment and we want to understand which forces are at
play and whether these forces can be understood in terms of
fundamental forces.

Our first step consists in identifying the problem. What we sketch
here may include a mix of experiment and theoretical simulations, or
just experiment or only theory.

## Identifying our System

Here we can ask questions like
1. What kind of object is moving

2. What kind of data do we have

3. How do we measure position, velocity, acceleration etc

4. Which initial conditions influence our system

5. Other aspects which allow us to identify the system

## Defining a Model

With our eventual data and observations we would now like to develop a
model for the system. In the end we want obviously to be able to
understand which forces are at play and how they influence our
specific system. That is, can we extract some deeper insights about a
system?

We need then to
1. Find the forces that act on our system

2. Introduce models for the forces

3. Identify the equations which can govern the system (Newton's second law for example)

4. More elements we deem important for defining our model

## Solving the Equations

With the model at hand, we can then solve the equations. In classical mechanics we normally end up  with solving sets of coupled ordinary differential equations or partial differential equations.
1. Using Newton's second law we have equations of the type $\boldsymbol{F}=m\boldsymbol{a}=md\boldsymbol{v}/dt$

2. We need to  define the initial conditions (typically the initial velocity and position as functions of time) and/or initial conditions and boundary conditions

3. The solution of the equations give us then the position, the velocity and other time-dependent quantities which may specify the motion of a given object.

We are not yet done. With our lovely solvers, we need to start thinking.

Now it is time to ask the big questions. What do our results mean? Can we give a simple interpretation in terms of fundamental laws?  What do our results mean? Are they correct?
Thus, typical questions we may ask are
1. Are our results for say $\boldsymbol{r}(t)$ valid?  Do we trust what we did?  Can you validate and verify the correctness of your results?

2. Evaluate the answers and their implications

3. Compare with experimental data if possible. Does our model make sense?

4. and obviously many other questions.

The analysis stage feeds back to the first stage. It may happen that
the data we had were not good enough, there could be large statistical
uncertainties. We may need to collect more data or perhaps we did a
sloppy job in identifying the degrees of freedom.

All these steps are essential elements in a scientific
enquiry. Hopefully, through a mix of numerical simulations, analytical
calculations and experiments we may gain a deeper insight about the
physics of a specific system.

Let us now remind ourselves of Newton's laws, since these are the laws of motion we will study in this course.

## Newton's Laws

When analyzing a physical system we normally start with distinguishing between the object we are studying (we will label this in more general terms as our **system**) and how this system interacts with the environment (which often means everything else!)

In our investigations we will thus analyze a specific physics problem in terms of the system and the environment.
In doing so we need to identify the forces that act on the system and assume that the
forces acting on the system must have a source, an identifiable cause in
the environment.

A force acting on for example a falling object must be related to an interaction with something in the environment.
This also means that we do not consider internal forces. The latter are forces between
one part of the object and another part. In this course we will mainly focus on external forces.

Forces are either contact forces or long-range forces.

Contact forces, as evident from the name, are forces that occur at the contact between
the system and the environment. Well-known long-range forces are the gravitional force and the electromagnetic force.

## Setting up a model for forces acting on an object

In order to set up the forces which act on an object, the following steps may be useful
1. Divide the problem into system and environment.

2. Draw a figure of the object and everything in contact with the object.

3. Draw a closed curve around the system.

4. Find contact points—these are the points where contact forces may act.

5. Give names and symbols to all the contact forces.

6. Identify the long-range forces.

7. Make a drawing of the object. Draw the forces as arrows, vectors, starting from where the force is acting. The direction of the vector(s) indicates the (positive) direction of the force. Try to make the length of the arrow indicate the relative magnitude of the forces.

8. Draw in the axes of the coordinate system. It is often convenient to make one axis parallel to the direction of motion. When you choose the direction of the axis you also choose the positive direction for the axis.

## Newton's Laws, the Second one first

Newton’s second law of motion: The force $\boldsymbol{F}$ on an object of inertial mass $m$
is related to the acceleration a of the object through

$$
\boldsymbol{F} = m\boldsymbol{a},
$$

where $\boldsymbol{a}$ is the acceleration.

Newton’s laws of motion are laws of nature that have been found by experimental
investigations and have been shown to hold up to continued experimental investigations.
Newton’s laws are valid over a wide range of length- and time-scales. We
use Newton’s laws of motion to describe everything from the motion of atoms to the
motion of galaxies.

The second law is a vector equation with the acceleration having the same
direction as the force. The acceleration is proportional to the force via the mass $m$ of the system under study.

Newton’s second law introduces a new property of an object, the so-called
inertial mass $m$. We determine the inertial mass of an object by measuring the
acceleration for a given applied force.

## Then the First Law

What happens if the net external force on a body is zero? Applying Newton’s second
law, we find:

$$
\boldsymbol{F} = 0 = m\boldsymbol{a},
$$

which gives using the definition of the acceleration

$$
\boldsymbol{a} = \frac{d\boldsymbol{v}}{dt}=0.
$$

The acceleration is zero, which means that the velocity of the object is constant. This
is often referred to as Newton’s first law. An object in a state of uniform motion tends to remain in
that state unless an external force changes its state of motion.
Why do we need a separate law for this? Is it not simply a special case of Newton’s
second law? Yes, Newton’s first law can be deduced from the second law as we have
illustrated. However, the first law is often used for a different purpose: Newton’s
First Law tells us about the limit of applicability of Newton’s Second law. Newton’s
Second law can only be used in reference systems where the First law is obeyed. But
is not the First law always valid? No! The First law is only valid in reference systems
that are not accelerated. If you observe the motion of a ball from an accelerating
car, the ball will appear to accelerate even if there are no forces acting on it. We call
systems that are not accelerating inertial systems, and Newton’s first law is often
called the law of inertia. Newton’s first and second laws of motion are only valid in
inertial systems.

A system is an inertial system if it is not accelerated. It means that the reference system
must not be accelerating linearly or rotating. Unfortunately, this means that most
systems we know are not really inertial systems. For example, the surface of the
Earth is clearly not an inertial system, because the Earth is rotating. The Earth is also
not an inertial system, because it ismoving in a curved path around the Sun. However,
even if the surface of the Earth is not strictly an inertial system, it may be considered
to be approximately an inertial system for many laboratory-size experiments.

## And finally the Third Law

If there is a force from object A on object B, there is also a force from object B on object A.
This fundamental principle of interactions is called Newton’s third law. We do not
know of any force that do not obey this law: All forces appear in pairs. Newton’s
third law is usually formulated as: For every action there is an equal and opposite
reaction.

## Motion of a Single Object

Here we consider the motion of a single particle moving under
the influence of some set of forces.  We will consider some problems where
the force does not depend on the position. In that case Newton's law
$m\dot{\boldsymbol{v}}=\boldsymbol{F}(\boldsymbol{v})$ is a first-order differential
equation and one solves for $\boldsymbol{v}(t)$, then moves on to integrate
$\boldsymbol{v}$ to get the position. In essentially all of these cases we cna find an analytical solution.

## Air Resistance in One Dimension

Air resistance tends to scale as the square of the velocity. This is
in contrast to many problems chosen for textbooks, where it is linear
in the velocity. The choice of a linear dependence is motivated by
mathematical simplicity (it keeps the differential equation linear)
rather than by physics. One can see that the force should be quadratic
in velocity by considering the momentum imparted on the air
molecules. If an object sweeps through a volume $dV$ of air in time
$dt$, the momentum imparted on the air is

$$
\begin{equation}
dP=\rho_m dV v,
\label{_auto1} \tag{1}
\end{equation}
$$

where $v$ is the velocity of the object and $\rho_m$ is the mass
density of the air. If the molecules bounce back as opposed to stop
you would double the size of the term. The opposite value of the
momentum is imparted onto the object itself. Geometrically, the
differential volume is

$$
\begin{equation}
dV=Avdt,
\label{_auto2} \tag{2}
\end{equation}
$$

where $A$ is the cross-sectional area and $vdt$ is the distance the
object moved in time $dt$.

## Resulting Acceleration
Plugging this into the expression above,

$$
\begin{equation}
\frac{dP}{dt}=-\rho_m A v^2.
\label{_auto3} \tag{3}
\end{equation}
$$

This is the force felt by the particle, and is opposite to its
direction of motion. Now, because air doesn't stop when it hits an
object, but flows around the best it can, the actual force is reduced
by a dimensionless factor $c_W$, called the drag coefficient.

$$
\begin{equation}
F_{\rm drag}=-c_W\rho_m Av^2,
\label{_auto4} \tag{4}
\end{equation}
$$

and the acceleration is

$$
\begin{eqnarray}
\frac{dv}{dt}=-\frac{c_W\rho_mA}{m}v^2.
\end{eqnarray}
$$

For a particle with initial velocity $v_0$, one can separate the $dt$
to one side of the equation, and move everything with $v$s to the
other side. We did this in our discussion of simple motion and will not repeat it here.

On more general terms,
for many systems, e.g. an automobile, there are multiple sources of
resistance. In addition to wind resistance, where the force is
proportional to $v^2$, there are dissipative effects of the tires on
the pavement, and in the axel and drive train. These other forces can
have components that scale proportional to $v$, and components that
are independent of $v$. Those independent of $v$, e.g. the usual
$f=\mu_K N$ frictional force you consider in your first Physics courses, only set in
once the object is actually moving. As speeds become higher, the $v^2$
components begin to dominate relative to the others. For automobiles
at freeway speeds, the $v^2$ terms are largely responsible for the
loss of efficiency. To travel a distance $L$ at fixed speed $v$, the
energy/work required to overcome the dissipative forces are $fL$,
which for a force of the form $f=\alpha v^n$ becomes

$$
\begin{eqnarray}
W=\int dx~f=\alpha v^n L.
\end{eqnarray}
$$

For $n=0$ the work is
independent of speed, but for the wind resistance, where $n=2$,
slowing down is essential if one wishes to reduce fuel consumption. It
is also important to consider that engines are designed to be most
efficient at a chosen range of power output. Thus, some cars will get
better mileage at higher speeds (They perform better at 50 mph than at
5 mph) despite the considerations mentioned above.

## Going Ballistic, Projectile Motion or a Softer Approach, Falling Raindrops

As an example of Newton's Laws we consider projectile motion (or a
falling raindrop or a ball we throw up in the air) with a drag force. Even though air resistance is
largely proportional to the square of the velocity, we will consider
the drag force to be linear to the velocity, $\boldsymbol{F}=-m\gamma\boldsymbol{v}$,
for the purposes of this exercise. The acceleration for a projectile moving upwards,
$\boldsymbol{a}=\boldsymbol{F}/m$, becomes

$$
\begin{eqnarray}
\frac{dv_x}{dt}=-\gamma v_x,\\
\nonumber
\frac{dv_y}{dt}=-\gamma v_y-g,
\end{eqnarray}
$$

and $\gamma$ has dimensions of inverse time.

If you on the other hand have a falling raindrop, how do these equations change? See for example Figure 2.1 in Taylor.
Let us stay with a ball which is thrown up in the air at $t=0$.

## Ways of solving these equations

We will go over two different ways to solve this equation. The first
by direct integration, and the second as a differential equation. To
do this by direct integration, one simply multiplies both sides of the
equations above by $dt$, then divide by the appropriate factors so
that the $v$s are all on one side of the equation and the $dt$ is on
the other. For the $x$ motion one finds an easily integrable equation,

$$
\begin{eqnarray}
\frac{dv_x}{v_x}&=&-\gamma dt,\\
\nonumber
\int_{v_{0x}}^{v_{x}}\frac{dv_x}{v_x}&=&-\gamma\int_0^{t}dt,\\
\nonumber
\ln\left(\frac{v_{x}}{v_{0x}}\right)&=&-\gamma t,\\
\nonumber
v_{x}(t)&=&v_{0x}e^{-\gamma t}.
\end{eqnarray}
$$

This is very much the result you would have written down
by inspection. For the $y$-component of the velocity,

$$
\begin{eqnarray}
\frac{dv_y}{v_y+g/\gamma}&=&-\gamma dt\\
\nonumber
\ln\left(\frac{v_{y}+g/\gamma}{v_{0y}-g/\gamma}\right)&=&-\gamma t_f,\\
\nonumber
v_{fy}&=&-\frac{g}{\gamma}+\left(v_{0y}+\frac{g}{\gamma}\right)e^{-\gamma t}.
\end{eqnarray}
$$

Whereas $v_x$ starts at some value and decays
exponentially to zero, $v_y$ decays exponentially to the terminal
velocity, $v_t=-g/\gamma$.

## Solving as differential equations

Although this direct integration is simpler than the method we invoke
below, the method below will come in useful for some slightly more
difficult differential equations in the future. The differential
equation for $v_x$ is straight-forward to solve. Because it is first
order there is one arbitrary constant, $A$, and by inspection the
solution is

$$
\begin{equation}
v_x=Ae^{-\gamma t}.
\label{_auto5} \tag{5}
\end{equation}
$$

The arbitrary constants for equations of motion are usually determined
by the initial conditions, or more generally boundary conditions. By
inspection $A=v_{0x}$, the initial $x$ component of the velocity.

## Differential Equations, contn

The differential equation for $v_y$ is a bit more complicated due to
the presence of $g$. Differential equations where all the terms are
linearly proportional to a function, in this case $v_y$, or to
derivatives of the function, e.g., $v_y$, $dv_y/dt$,
$d^2v_y/dt^2\cdots$, are called linear differential equations. If
there are terms proportional to $v^2$, as would happen if the drag
force were proportional to the square of the velocity, the
differential equation is not longer linear. Because this expression
has only one derivative in $v$ it is a first-order linear differential
equation. If a term were added proportional to $d^2v/dt^2$ it would be
a second-order differential equation.  In this case we have a term
completely independent of $v$, the gravitational acceleration $g$, and
the usual strategy is to first rewrite the equation with all the
linear terms on one side of the equal sign,

$$
\begin{equation}
\frac{dv_y}{dt}+\gamma v_y=-g.
\label{_auto6} \tag{6}
\end{equation}
$$

## Splitting into two parts

Now, the solution to the equation can be broken into two
parts. Because this is a first-order differential equation we know
that there will be one arbitrary constant. Physically, the arbitrary
constant will be determined by setting the initial velocity, though it
could be determined by setting the velocity at any given time. Like
most differential equations, solutions are not "solved". Instead,
one guesses at a form, then shows the guess is correct. For these
types of equations, one first tries to find a single solution,
i.e. one with no arbitrary constants. This is called the {\it
particular} solution, $y_p(t)$, though it should really be called
"a" particular solution because there are an infinite number of such
solutions. One then finds a solution to the {\it homogenous} equation,
which is the equation with zero on the right-hand side,

$$
\begin{equation}
\frac{dv_{y,h}}{dt}+\gamma v_{y,h}=0.
\label{_auto7} \tag{7}
\end{equation}
$$

Homogenous solutions will have arbitrary constants.

The particular solution will solve the same equation as the original
general equation

$$
\begin{equation}
\frac{dv_{y,p}}{dt}+\gamma v_{y,p}=-g.
\label{_auto8} \tag{8}
\end{equation}
$$

However, we don't need find one with arbitrary constants. Hence, it is
called a **particular** solution.

The sum of the two,

$$
\begin{equation}
v_y=v_{y,p}+v_{y,h},
\label{_auto9} \tag{9}
\end{equation}
$$

is a solution of the total equation because of the linear nature of
the differential equation. One has now found a *general* solution
encompassing all solutions, because it both satisfies the general
equation (like the particular solution), and has an arbitrary constant
that can be adjusted to fit any initial condition (like the homogneous
solution). If the equation were not linear, e.g if there were a term
such as $v_y^2$ or $v_y\dot{v}_y$, this technique would not work.

## More details

Returning to the example above, the homogenous solution is the same as
that for $v_x$, because there was no gravitational acceleration in
that case,

$$
\begin{equation}
v_{y,h}=Be^{-\gamma t}.
\label{_auto10} \tag{10}
\end{equation}
$$

In this case a particular solution is one with constant velocity,

$$
\begin{equation}
v_{y,p}=-g/\gamma.
\label{_auto11} \tag{11}
\end{equation}
$$

Note that this is the terminal velocity of a particle falling from a
great height. The general solution is thus,

$$
\begin{equation}
v_y=Be^{-\gamma t}-g/\gamma,
\label{_auto12} \tag{12}
\end{equation}
$$

and one can find $B$ from the initial velocity,

$$
\begin{equation}
v_{0y}=B-g/\gamma,~~~B=v_{0y}+g/\gamma.
\label{_auto13} \tag{13}
\end{equation}
$$

Plugging in the expression for $B$ gives the $y$ motion given the initial velocity,

$$
\begin{equation}
v_y=(v_{0y}+g/\gamma)e^{-\gamma t}-g/\gamma.
\label{_auto14} \tag{14}
\end{equation}
$$

It is easy to see that this solution has $v_y=v_{0y}$ when $t=0$ and
$v_y=-g/\gamma$ when $t\rightarrow\infty$.

One can also integrate the two equations to find the coordinates $x$
and $y$ as functions of $t$,

$$
\begin{eqnarray}
x&=&\int_0^t dt'~v_{0x}(t')=\frac{v_{0x}}{\gamma}\left(1-e^{-\gamma t}\right),\\
\nonumber
y&=&\int_0^t dt'~v_{0y}(t')=-\frac{gt}{\gamma}+\frac{v_{0y}+g/\gamma}{\gamma}\left(1-e^{-\gamma t}\right).
\end{eqnarray}
$$

If the question was to find the position at a time $t$, we would be
finished. However, the more common goal in a projectile equation
problem is to find the range, i.e. the distance $x$ at which $y$
returns to zero. For the case without a drag force this was much
simpler. The solution for the $y$ coordinate would have been
$y=v_{0y}t-gt^2/2$. One would solve for $t$ to make $y=0$, which would
be $t=2v_{0y}/g$, then plug that value for $t$ into $x=v_{0x}t$ to
find $x=2v_{0x}v_{0y}/g=v_0\sin(2\theta_0)/g$. One follows the same
steps here, except that the expression for $y(t)$ is more
complicated. Searching for the time where $y=0$, and we get

$$
\begin{equation}
0=-\frac{gt}{\gamma}+\frac{v_{0y}+g/\gamma}{\gamma}\left(1-e^{-\gamma t}\right).
\label{_auto15} \tag{15}
\end{equation}
$$

This cannot be inverted into a simple expression $t=\cdots$. Such
expressions are known as "transcendental equations", and are not the
rare instance, but are the norm. In the days before computers, one
might plot the right-hand side of the above graphically as
a function of time, then find the point where it crosses zero.

Now, the most common way to solve for an equation of the above type
would be to apply Newton's method numerically. This involves the
following algorithm for finding solutions of some equation $F(t)=0$.

1. First guess a value for the time, $t_{\rm guess}$.

2. Calculate $F$ and its derivative, $F(t_{\rm guess})$ and $F'(t_{\rm guess})$.

3. Unless you guessed perfectly, $F\ne 0$, and assuming that $\Delta F\approx F'\Delta t$, one would choose

4. $\Delta t=-F(t_{\rm guess})/F'(t_{\rm guess})$.

5. Now repeat step 1, but with $t_{\rm guess}\rightarrow t_{\rm guess}+\Delta t$.

If the $F(t)$ were perfectly linear in $t$, one would find $t$ in one
step. Instead, one typically finds a value of $t$ that is closer to
the final answer than $t_{\rm guess}$. One breaks the loop once one
finds $F$ within some acceptable tolerance of zero. A program to do
this will be added shortly.

## Motion in a Magnetic Field

Another example of a velocity-dependent force is magnetism,

$$
\begin{eqnarray}
\boldsymbol{F}&=&q\boldsymbol{v}\times\boldsymbol{B},\\
\nonumber
F_i&=&q\sum_{jk}\epsilon_{ijk}v_jB_k.
\end{eqnarray}
$$

For a uniform field in the $z$ direction $\boldsymbol{B}=B\hat{z}$, the force can only have $x$ and $y$ components,

$$
\begin{eqnarray}
F_x&=&qBv_y\\
\nonumber
F_y&=&-qBv_x.
\end{eqnarray}
$$

The differential equations are

$$
\begin{eqnarray}
\dot{v}_x&=&\omega_c v_y,\omega_c= qB/m\\
\nonumber
\dot{v}_y&=&-\omega_c v_x.
\end{eqnarray}
$$

One can solve the equations by taking time derivatives of either equation, then substituting into the other equation,

$$
\begin{eqnarray}
\ddot{v}_x=\omega_c\dot{v_y}=-\omega_c^2v_x,\\
\nonumber
\ddot{v}_y&=&-\omega_c\dot{v}_x=-\omega_cv_y.
\end{eqnarray}
$$

The solution to these equations can be seen by inspection,

$$
\begin{eqnarray}
v_x&=&A\sin(\omega_ct+\phi),\\
\nonumber
v_y&=&A\cos(\omega_ct+\phi).
\end{eqnarray}
$$

One can integrate the equations to find the positions as a function of time,

$$
\begin{eqnarray}
x-x_0&=&\int_{x_0}^x dx=\int_0^t dt v(t)\\
\nonumber
&=&\frac{-A}{\omega_c}\cos(\omega_ct+\phi),\\
\nonumber
y-y_0&=&\frac{A}{\omega_c}\sin(\omega_ct+\phi).
\end{eqnarray}
$$

The trajectory is a circle centered at $x_0,y_0$ with amplitude $A$ rotating in the clockwise direction.

The equations of motion for the $z$ motion are

$$
\begin{equation}
\dot{v_z}=0,
\label{_auto16} \tag{16}
\end{equation}
$$

which leads to

$$
\begin{equation}
z-z_0=V_zt.
\label{_auto17} \tag{17}
\end{equation}
$$

Added onto the circle, the motion is helical.

Note that the kinetic energy,

$$
\begin{equation}
T=\frac{1}{2}m(v_x^2+v_y^2+v_z^2)=\frac{1}{2}m(\omega_c^2A^2+V_z^2),
\label{_auto18} \tag{18}
\end{equation}
$$

is constant. This is because the force is perpendicular to the
velocity, so that in any differential time element $dt$ the work done
on the particle $\boldsymbol{F}\cdot{dr}=dt\boldsymbol{F}\cdot{v}=0$.

One should think about the implications of a velocity dependent
force. Suppose one had a constant magnetic field in deep space. If a
particle came through with velocity $v_0$, it would undergo cyclotron
motion with radius $R=v_0/\omega_c$. However, if it were still its
motion would remain fixed. Now, suppose an observer looked at the
particle in one reference frame where the particle was moving, then
changed their velocity so that the particle's velocity appeared to be
zero. The motion would change from circular to fixed. Is this
possible?

The solution to the puzzle above relies on understanding
relativity. Imagine that the first observer believes $\boldsymbol{B}\ne 0$ and
that the electric field $\boldsymbol{E}=0$. If the observer then changes
reference frames by accelerating to a velocity $\boldsymbol{v}$, in the new
frame $\boldsymbol{B}$ and $\boldsymbol{E}$ both change. If the observer moved to the
frame where the charge, originally moving with a small velocity $v$,
is now at rest, the new electric field is indeed $\boldsymbol{v}\times\boldsymbol{B}$,
which then leads to the same acceleration as one had before. If the
velocity is not small compared to the speed of light, additional
$\gamma$ factors come into play,
$\gamma=1/\sqrt{1-(v/c)^2}$. Relativistic motion will not be
considered in this course.

## Sliding Block tied to a Wall

Another classical case is that of simple harmonic oscillations, here represented by a block sliding on a horizontal frictionless surface. The block is tied to a wall with a spring. If the spring is not compressed or stretched too far, the force on the block at a given position $x$ is

$$
F=-kx.
$$

The negative sign means that the force acts to restore the object to an equilibrium position. Newton's equation of motion for this idealized system is then

$$
m\frac{d^2x}{dt^2}=-kx,
$$

or we could rephrase it as

$$
\frac{d^2x}{dt^2}=-\frac{k}{m}x=-\omega_0^2x,
\label{eq:newton1} \tag{19}
$$

with the angular frequency $\omega_0^2=k/m$.

The above differential equation has the advantage that it can be solved  analytically with solutions on the form

$$
x(t)=Acos(\omega_0t+\nu),
$$

where $A$ is the amplitude and $\nu$ the phase constant.   This provides in turn an important test for the numerical
solution and the development of a program for more complicated cases which cannot be solved analytically.

With the position $x(t)$ and the velocity  $v(t)=dx/dt$ we can reformulate Newton's equation in the following way

$$
\frac{dx(t)}{dt}=v(t),
$$

and

$$
\frac{dv(t)}{dt}=-\omega_0^2x(t).
$$

We are now going to solve these equations using first the standard forward Euler  method. Later we will try to improve upon this.

Before proceeding however, it is important to note that in addition to the exact solution, we have at least two further tests which can be used to check our solution.

Since functions like $cos$ are periodic with a period $2\pi$, then the solution $x(t)$ has also to be periodic. This means that

$$
x(t+T)=x(t),
$$

with $T$ the period defined as

$$
T=\frac{2\pi}{\omega_0}=\frac{2\pi}{\sqrt{k/m}}.
$$

Observe that $T$ depends only on $k/m$ and not on the amplitude of the solution.

In addition to the periodicity test, the total energy has also to be conserved.

Suppose we choose the initial conditions

$$
x(t=0)=1\hspace{0.1cm} \mathrm{m}\hspace{1cm} v(t=0)=0\hspace{0.1cm}\mathrm{m/s},
$$

meaning that block is at rest at $t=0$ but with a potential energy

$$
E_0=\frac{1}{2}kx(t=0)^2=\frac{1}{2}k.
$$

The total energy at any time $t$ has however to be conserved, meaning that our solution has to fulfil the condition

$$
E_0=\frac{1}{2}kx(t)^2+\frac{1}{2}mv(t)^2.
$$

We will derive this equation in our discussion on [energy conservation](https://mhjensen.github.io/Physics321/doc/pub/energyconserv/html/energyconserv.html).

An algorithm which implements these equations is included below.
 * Choose the initial position and speed, with the most common choice $v(t=0)=0$ and some fixed value for the position.

 * Choose the method you wish to employ in solving the problem.

 * Subdivide the time interval $[t_i,t_f] $ into a grid with step size

$$
h=\frac{t_f-t_i}{N},
$$

where $N$ is the number of mesh points.
 * Calculate now the total energy given by

$$
E_0=\frac{1}{2}kx(t=0)^2=\frac{1}{2}k.
$$

* Choose ODE solver to obtain $x_{i+1}$ and $v_{i+1}$ starting from the previous values $x_i$ and $v_i$.

 * When we have computed $x(v)_{i+1}$ we upgrade  $t_{i+1}=t_i+h$.

 * This iterative  process continues till we reach the maximum time $t_f$.

 * The results are checked against the exact solution. Furthermore, one has to check the stability of the numerical solution against the chosen number of mesh points $N$.

The following python program ( code will be added shortly)

```
#
# This program solves Newtons equation for a block sliding on
# an horizontal frictionless surface.
# The block is tied to the wall with a spring, so N's eq takes the form:
#
#  m d^2x/dt^2 = - kx
#
# In order to make the solution dimless, we set k/m = 1.
# This results in two coupled diff. eq's that may be written as:
#
#  dx/dt = v
#  dv/dt = -x
#
# The user has to specify the initial velocity and position,
# and the number of steps. The time interval is fixed to
# t \in [0, 4\pi) (two periods)
#
```

## The classical pendulum and scaling the equations

The angular equation of motion of the pendulum is given by
Newton's equation and with no external force it reads

$$
\begin{equation}
  ml\frac{d^2\theta}{dt^2}+mgsin(\theta)=0,
\label{_auto19} \tag{20}
\end{equation}
$$

with an angular velocity and acceleration given by

$$
\begin{equation}
     v=l\frac{d\theta}{dt},
\label{_auto20} \tag{21}
\end{equation}
$$

and

$$
\begin{equation}
     a=l\frac{d^2\theta}{dt^2}.
\label{_auto21} \tag{22}
\end{equation}
$$

## More on the Pendulum

We do however expect that the motion will gradually come to an end due a viscous drag torque acting on the pendulum.
In the presence of the drag, the above equation becomes

$$
\begin{equation}
   ml\frac{d^2\theta}{dt^2}+\nu\frac{d\theta}{dt}  +mgsin(\theta)=0, \label{eq:pend1} \tag{23}
\end{equation}
$$

where $\nu$ is now a positive constant parameterizing the viscosity
of the medium in question. In order to maintain the motion against
viscosity, it is necessary to add some external driving force.
We choose here a periodic driving force. The last equation becomes then

$$
\begin{equation}
   ml\frac{d^2\theta}{dt^2}+\nu\frac{d\theta}{dt}  +mgsin(\theta)=Asin(\omega t), \label{eq:pend2} \tag{24}
\end{equation}
$$

with $A$ and $\omega$ two constants representing the amplitude and
the angular frequency respectively. The latter is called the driving frequency.

## More on the Pendulum

We define

$$
\omega_0=\sqrt{g/l},
$$

the so-called natural frequency and the new dimensionless quantities

$$
\hat{t}=\omega_0t,
$$

with the dimensionless driving frequency

$$
\hat{\omega}=\frac{\omega}{\omega_0},
$$

and introducing the quantity $Q$, called the *quality factor*,

$$
Q=\frac{mg}{\omega_0\nu},
$$

and the dimensionless amplitude

$$
\hat{A}=\frac{A}{mg}
$$

We have

$$
\frac{d^2\theta}{d\hat{t}^2}+\frac{1}{Q}\frac{d\theta}{d\hat{t}}
     +sin(\theta)=\hat{A}cos(\hat{\omega}\hat{t}).
$$

This equation can in turn be recast in terms of two coupled first-order differential equations as follows

$$
\frac{d\theta}{d\hat{t}}=\hat{v},
$$

and

$$
\frac{d\hat{v}}{d\hat{t}}=-\frac{\hat{v}}{Q}-sin(\theta)+\hat{A}cos(\hat{\omega}\hat{t}).
$$

These are the equations to be solved.  The factor $Q$ represents the number of oscillations of the undriven system that must occur before  its energy is significantly reduced due to the viscous drag. The  amplitude $\hat{A}$ is measured in units of the maximum possible  gravitational torque while $\hat{\omega}$ is the angular frequency of the external torque measured in units of the pendulum's natural frequency.


*This notebook contains material from [CBE40455-2020](https://jckantor.github.io/CBE40455-2020);
content is available [on Github](https://github.com/jckantor/CBE40455-2020.git).*

< [3.9 Refinements of a Grocery Store Checkout Operation](https://jckantor.github.io/CBE40455-2020/03.09-Refinements-to-the-Grocery-Store-Checkout-Operation.html) | [Contents](toc.html) | [3.11 Batch Chemical Process](https://jckantor.github.io/CBE40455-2020/03.11-Project-Batch-Chemical-Process.html) >

# 3.10 Objected-Oriented Simulation

Up to this point we have been using Python generators and shared resources as the building blocks for simulations of complex systems. This can be  effective, particularly if the individual agents do not require access to the internal state of other agents. But there are situations where the action of an agent depends on the state or properties of another agent in the simulation. For example, consider this discussion question from the Grocery store checkout example:

>Suppose we were to change one or more of the lanes to a express lanes which handle only with a small number of items, say five or fewer. How would you expect this to change average waiting time? This is a form of prioritization ... are there other prioritizations that you might consider?

The customer action depends the item limit parameter associated with a checkout lane. This is a case where the action of one agent depends on a property of another. The shared resources builtin to the SimPy library provide some functionality in this regard, but how do add this to the simulations we write?

The good news is that Python offers a rich array of object oriented programming features well suited to this purpose. The SymPy documentation provides excellent examples of how to create Python objects for use in SymPy. The bad news is that object oriented programming in Python -- while straightforward compared to many other programming languages -- constitutes a steep learning curve for students unfamiliar with the core concepts.

Fortunately, since the introduction of Python 3.7 in 2018, the standard libraries for Python have included a simplified method for creating and using Python classes. Using [dataclass](https://realpython.com/python-data-classes/), it easy to create objects for SymPy simulations that retain the benefits of object oriented programming without all of the coding overhead.

The purpose of this notebook is to introduce the use of `dataclass` in creating SymPy simulations. To the best of the author's knowledge, this is a novel use of `dataclass` and the only example of which the author is aware.

## 3.10.1 Installations and imports

```python
!pip install sympy
```

    Requirement already satisfied: sympy in /Users/jeff/opt/anaconda3/lib/python3.7/site-packages (1.5.1)
    Requirement already satisfied: mpmath>=0.19 in /Users/jeff/opt/anaconda3/lib/python3.7/site-packages (from sympy) (1.1.0)

```python
%matplotlib inline
import matplotlib.pyplot as plt
import numpy as np
import random
import simpy
import pandas as pd
from dataclasses import dataclass
```

```python
import sys
print(sys.version)
```

    3.7.4 (default, Aug 13 2019, 15:17:50)
    [Clang 4.0.1 (tags/RELEASE_401/final)]

Additional imports are from the `dataclasses` library that has been part of the standard Python distribution since version 3.7. Here we import `dataclass` and `field`.

```python
from dataclasses import dataclass, field
```

## 3.10.2 Introduction to `dataclass`

Tutorials and additional documentation:

* [The Ultimate Guide to Data Classes in Python 3.7](https://realpython.com/python-data-classes/):  Tutorial article from ReaalPython.com
* [dataclasses — Data Classes](https://docs.python.org/3/library/dataclasses.html):  Official Python documentation.
* [Data Classes in Python](https://towardsdatascience.com/data-classes-in-python-8d1a09c1294b): Tutorial from TowardsDataScience.com

### 3.10.2.1 Creating a `dataclass`

A `dataclass` defines a new class of Python objects. A `dataclass` object takes care of several routine things that you would otherwise have to code, such as creating instances of an object, testing for equality, and other aspects.

As an example, the following cell shows how to define a dataclass corresponding to a hypothetical Student object. The Student object maintains data associated with instances of a student. The dataclass also defines a function associated with the object.

```python
from dataclasses import dataclass

@dataclass
class Student():
    name: str
    graduation_class: int
    dorm: str

    def print_name(self):
        print(f"{self.name} (Class of {self.graduation_class})")
```

Let's create an instance of the Student object.

```python
sam = Student("Sam Jones", 2024, "Alumni")
```

Let's see how the `print_name()` function works.

```python
sam.print_name()
```

    Sam Jones (Class of 2024)

The next cell shows how to create a list of students, and how to iterate over a list of students.

```python
# create a list of students
students = [
    Student("Sam Jones", 2024, "Alumni"),
    Student("Becky Smith", 2023, "Howard"),
]

# iterate over the list of students to print all of their names
for student in students:
    student.print_name()
    print(student.dorm)
```

    Sam Jones (Class of 2024)
    Alumni
    Becky Smith (Class of 2023)
    Howard

Here are a few details you need to use `dataclass` effectively:

* The `class` statement is standard statement for creating a new class of Python objects. The preceding `@dataclass` is a Python 'decorator'. Decorators are Python functions that modify the behavior of subsequent statements. In this case, the `@dataclass` decorator modifies `class` to provide a streamlined syntax for implementing classes.
* A Python class names begin with a capital letter. In this case `Student` is the class name.
* The lines following the the class statement declare parameters that will be used by the new class. The parameters can be specified when you create an instance of the dataclass.
* Each paraameter is followed by type 'hint'.  Commonly used type hints are `int`, `float`, `bool`, and `str`. Use the keyword `any` you don't know or can't specify a particular type. Type hints are actually used by type-checking tools and ignored by the python interpreter.
* Following the parameters, write any functions or generators that you may wish to define for the new class. To access variables unique to an instance of the class, preceed the parameter name with `self`.

### 3.10.2.2 Specifying parameter values

There are different ways of specifying the parameter values assigned to an instance of a dataclass. Here are three particular methods:

* Specify the parameter value when creating a new instance. This is what was done in the Student example above.
* Provide a default values determined when the dataclass is defined.
* Provide a default_factory method to create a parameter value when an instance of the dataclass is created.

#### 3.10.2.2.1 Specifying a parameter value when creating a new instance

Parameter values can be specified when creating an instance of a dataclass. The parameter values can be specified by position or by name as shown below.

```python
from dataclasses import dataclass

@dataclass
class Student():
    name: str
    graduation_year: int
    dorm: str

    def print_name(self):
        print(f"{self.name} (Class of {self.graduation_year})")

sam = Student("Sam Jones", 2031, "Alumni")
sam.print_name()

gilda = Student(name="Gilda Radner", graduation_year=2030, dorm="Howard")
gilda.print_name()
```

    Sam Jones (Class of 2031)
    Gilda Radner (Class of 2030)

#### 3.10.2.2.2 Setting default parameter values

Setting a default value for a parameter can save extra typing or coding. More importantly, setting default values makes it easier to maintain and adapt code for other applications, and is a convenient way to handle missing data.

There are two ways to set default parameter values. For str, int, float, bool, tuple (the immutable types in Python), a default value can be set using `=` as shown in the next cell.

```python
from dataclasses import dataclass

@dataclass
class Student():
    name: str = None
    graduation_year: int = None
    dorm: str = None

    def print_name(self):
        print(f"{self.name} (Class of {self.graduation_year})")

jdoe = Student(name="John Doe", dorm="Alumni")
jdoe.print_name()
```

    John Doe (Class of None)

Default parameter values are restricted to 'immutable' types. This technical restriction eliminiates the error-prone practice of use mutable objects, such as lists, as defaults. The difficulty with setting defaults for mutable objects is that all instances of the dataclass share the same value. If one instance of the object changes that value, then all other instances are affected. This leads to unpredictable behavior, and is a particularly nasty bug to uncover and fix.

There are two ways to provide defaults for mutable parameters such as lists, sets, dictionaries, or arbitrary Python objects.

The more direct way is to specify a function for constucting the default parameter value using the `field` statement with the `default_factory` option. The default_factory is called when a new instance of the dataclass is created. The function must take no arguments and must return a value that will be assigned to the designated parameter. Here's an example.

```python
from dataclasses import dataclass

@dataclass
class Student():
    name: str = None
    graduation_year: int = None
    dorm: str = None
    majors: list = field(default_factory=list)

    def print_name(self):
        print(f"{self.name} (Class of {self.graduation_year})")

    def print_majors(self):
        for n, major in enumerate(self.majors):
            print(f"    {n+1}. {major}")

jdoe = Student(name="John Doe", dorm="Alumni", majors=["Math", "Chemical Engineering"])
jdoe.print_name()
jdoe.print_majors()

Student().print_majors()
```

    John Doe (Class of None)
        1. Math
        2. Chemical Engineering

#### 3.10.2.2.3 Initializing a dataclass with __post_init__(self)

Frequently there are additional steps to complete when creating a new instance of a dataclass. For that purpose, a dataclass may contain an
optional function with the special name `__post_init__(self)`. If present, that function is run automatically following the creation of a new instance. This feature will be demonstrated in following reimplementation of the grocery store checkout operation.

## 3.10.3 Using `dataclass` with Simpy

### 3.10.3.1 Step 0. A simple model

To demonstrate the use of classes in SimPy simulations, let's begin with a simple model of a clock using generators.

```python
import simpy

def clock(id="", t_step=1.0):
    while True:
        print(id, env.now)
        yield env.timeout(t_step)

env = simpy.Environment()
env.process(clock("A"))
env.process(clock("B", 1.5))
env.run(until=5.0)
```

    A 0
    B 0
    A 1.0
    B 1.5
    A 2.0
    B 3.0
    A 3.0
    A 4.0
    B 4.5

### 3.10.3.2 Step 1. Embed the generator inside of a class

As a first step, we rewrite the generator as a Python dataclass named `Clock`. The parameters are given default values, and the generator is incorporated within the Clock object. Note the use of `self` to refer to parameters specific to an instance of the class.

```python
import simpy
from dataclasses import dataclass

@dataclass
class Clock():
    id: str = ""
    t_step: float = 1.0

    def process(self):
        while True:
            print(self.id, env.now)
            yield env.timeout(self.t_step)

env = simpy.Environment()
env.process(Clock("A").process())
env.process(Clock("B", 1.5).process())
env.run(until=5)
```

    A 0
    B 0
    A 1.0
    B 1.5
    A 2.0
    B 3.0
    A 3.0
    A 4.0
    B 4.5

### 3.10.3.3 Step 2. Eliminate (if possible) global variables

Our definition of clock requires the simulation environment to have a specific name `env`, and assumes env is a global variable. That's generally not a good coding practice because it imposes an assumption on any user of the class, and exposes the internal coding of the class. A much better practice is to use class parameters to pass this data through a well defined interface to the class.

```python
import simpy
from dataclasses import dataclass

@dataclass
class Clock():
    env: simpy.Environment
    id: str = ""
    t_step: float = 1.0

    def process(self):
        while True:
            print(self.id, self.env.now)
            yield self.env.timeout(self.t_step)

env = simpy.Environment()
env.process(Clock(env, "A").process())
env.process(Clock(env, "B", 1.5).process())
env.run(until=10)
```

    A 0
    B 0
    A 1.0
    B 1.5
    A 2.0
    B 3.0
    A 3.0
    A 4.0
    B 4.5
    A 5.0
    B 6.0
    A 6.0
    A 7.0
    B 7.5
    A 8.0
    B 9.0
    A 9.0

### 3.10.3.4 Step 3. Encapsulate initializations inside __post_init__

```python
import simpy
from dataclasses import dataclass

@dataclass
class Clock():
    env: simpy.Environment
    id: str = ""
    t_step: float = 1.0

    def __post_init__(self):
        self.env.process(self.process())

    def process(self):
        while True:
            print(self.id, self.env.now)
            yield self.env.timeout(self.t_step)

env = simpy.Environment()
Clock(env, "A")
Clock(env, "B", 1.5)
env.run(until=5)
```

    A 0
    B 0
    A 1.0
    B 1.5
    A 2.0
    B 3.0
    A 3.0
    A 4.0
    B 4.5

## 3.10.4 Grocery Store Model

Let's review our model for the grocery store checkout operations. There are multiple checkout lanes, each with potentially different characteristics. With generators we were able to implement differences in the time required to scan items. But another parameter, a limit on number of items that could be checked out in a lane, required a new global list. The reason was the need to access that parameter, something that a generator doesn't allow. This is where classes become important building blocks in creating more complex simulations.

Our new strategy will be encapsulate the generator inside of a dataclass object. Here's what we'll ask each class definition to do:

* Create a parameter corresponding to the simulation environment.  This makes our classes reusable in other simulations by eliminating a reference to a globall variable.
* Create parameters with reasonable defaults values.
* Initialize any objects used within the class.
* Register the class generator with the simulation environment.

```python
from dataclasses import dataclass

# create simulation models
@dataclass
class Checkout():
    env: simpy.Environment
    lane: simpy.Store =  None
    t_item: float = 1/10
    item_limit: int = 25
    t_payment: float = 2.0

    def __post_init__(self):
        self.lane = simpy.Store(self.env)
        self.env.process(self.process())

    def process(self):
        while True:
            customer_id, cart, enter_time = yield self.lane.get()
            wait_time = env.now - enter_time
            yield env.timeout(self.t_payment + cart*self.t_item)
            customer_log.append([customer_id, cart, enter_time, wait_time, env.now])

@dataclass
class CustomerGenerator():
    env: simpy.Environment
    rate: float = 1.0
    customer_id: int = 1

    def __post_init__(self):
        self.env.process(self.process())

    def process(self):
        while True:
            yield env.timeout(random.expovariate(self.rate))
            cart = random.randint(1, 25)
            available_checkouts = [checkout for checkout in checkouts if cart <= checkout.item_limit]
            checkout = min(available_checkouts, key=lambda checkout: len(checkout.lane.items))
            yield checkout.lane.put([self.customer_id, cart, env.now])
            self.customer_id += 1

def lane_logger(t_sample=0.1):
    while True:
        lane_log.append([env.now] + [len(checkout.lane.items) for checkout in checkouts])
        yield env.timeout(t_sample)

# create simulation environment
env = simpy.Environment()

# create simulation objects (agents)
CustomerGenerator(env)
checkouts = [
    Checkout(env, t_item=1/5, item_limit=25),
    Checkout(env, t_item=1/5, item_limit=25),
    Checkout(env, item_limit=5),
    Checkout(env),
    Checkout(env),
]
env.process(lane_logger())

# run process
customer_log = []
lane_log = []
env.run(until=600)
```

```python
def visualize():

    # extract lane data
    lane_df = pd.DataFrame(lane_log, columns = ["time"] + [f"lane {n}" for n in range(0, len(checkouts))])
    lane_df = lane_df.set_index("time")

    customer_df = pd.DataFrame(customer_log, columns = ["customer id", "cart items", "enter", "wait", "leave"])
    customer_df["elapsed"] = customer_df["leave"] - customer_df["enter"]

    # compute kpi's
    print(f"Average waiting time = {customer_df['wait'].mean():5.2f} minutes")
    print(f"\nAverage lane queue \n{lane_df.mean()}")
    print(f"\nOverall aaverage lane queue \n{lane_df.mean().mean():5.4f}")

    # plot results
    fig, ax = plt.subplots(3, 1, figsize=(12, 7))
    ax[0].plot(lane_df)
    ax[0].set_xlabel("time / min")
    ax[0].set_title("length of checkout lanes")
    ax[0].legend(lane_df.columns)

    ax[1].bar(customer_df["customer id"], customer_df["wait"])
    ax[1].set_xlabel("customer id")
    ax[1].set_ylabel("minutes")
    ax[1].set_title("customer waiting time")

    ax[2].bar(customer_df["customer id"], customer_df["elapsed"])
    ax[2].set_xlabel("customer id")
    ax[2].set_ylabel("minutes")
    ax[2].set_title("total elapsed time")
    plt.tight_layout()

visualize()
```

## 3.10.5 Customers as agents

```python
from dataclasses import dataclass

# create simulation models
@dataclass
class Checkout():
    env: simpy.Environment
    lane: simpy.Store =  None
    t_item: float = 1/10
    item_limit: int = 25
    t_payment: float = 2.0

    def __post_init__(self):
        self.lane = simpy.Store(self.env)
        self.env.process(self.process())

    def process(self):
        while True:
            customer_id, cart, enter_time = yield self.lane.get()
            wait_time = env.now - enter_time
            yield env.timeout(self.t_payment + cart*self.t_item)
            customer_log.append([customer_id, cart, enter_time, wait_time, env.now])

@dataclass
class CustomerGenerator():
    env: simpy.Environment
    rate: float = 1.0
    customer_id: int = 1

    def __post_init__(self):
        self.env.process(self.process())

    def process(self):
        while True:
            yield env.timeout(random.expovariate(self.rate))
            Customer(self.env, self.customer_id)
            self.customer_id += 1

@dataclass
class Customer():
    env: simpy.Environment
    id: int = 0

    def __post_init__(self):
        self.cart = random.randint(1, 25)
        self.env.process(self.process())

    def process(self):
        available_checkouts = [checkout for checkout in checkouts if self.cart <= checkout.item_limit]
        checkout = min(available_checkouts, key=lambda checkout: len(checkout.lane.items))
        yield checkout.lane.put([self.id, self.cart, env.now])

def lane_logger(t_sample=0.1):
    while True:
        lane_log.append([env.now] + [len(checkout.lane.items) for checkout in checkouts])
        yield env.timeout(t_sample)

# create simulation environment
env = simpy.Environment()

# create simulation objects (agents)
CustomerGenerator(env)
checkouts = [
    Checkout(env, t_item=1/5, item_limit=25),
    Checkout(env, t_item=1/5, item_limit=25),
    Checkout(env, item_limit=5),
    Checkout(env),
    Checkout(env),
]
env.process(lane_logger())

# run process
customer_log = []
lane_log = []
env.run(until=600)

visualize()
```

## 3.10.6 Creating Smart Objects

```python
from dataclasses import dataclass, field
import pandas as pd

# create simulation models
@dataclass
class Checkout():
    lane: simpy.Store
    t_item: float = 1/10
    item_limit: int = 25

    def process(self):
        while True:
            customer_id, cart, enter_time = yield self.lane.get()
            wait_time = env.now - enter_time
            yield env.timeout(t_payment + cart*self.t_item)
            customer_log.append([customer_id, cart, enter_time, wait_time, env.now])

@dataclass
class CustomerGenerator():
    rate: float = 1.0
    customer_id: int = 1

    def process(self):
        while True:
            yield env.timeout(random.expovariate(self.rate))
            cart = random.randint(1, 25)
            available_checkouts = [checkout for checkout in checkouts if cart <= checkout.item_limit]
            checkout = min(available_checkouts, key=lambda checkout: len(checkout.lane.items))
            yield checkout.lane.put([self.customer_id, cart, env.now])
            self.customer_id += 1

@dataclass
class LaneLogger():
    lane_log: list = field(default_factory=list)  # this creates a variable that can be modified
    t_sample: float = 0.1
    lane_df: pd.DataFrame = field(default_factory=pd.DataFrame)

    def process(self):
        while True:
            self.lane_log.append([env.now] + [len(checkout.lane.items) for checkout in checkouts])
            yield env.timeout(self.t_sample)

    def report(self):
        self.lane_df = pd.DataFrame(self.lane_log, columns = ["time"] + [f"lane {n}" for n in range(0, N)])
        self.lane_df = self.lane_df.set_index("time")
        print(f"\nAverage lane queue \n{self.lane_df.mean()}")
        print(f"\nOverall average lane queue \n{self.lane_df.mean().mean():5.4f}")

    def plot(self):
        self.lane_df = pd.DataFrame(self.lane_log, columns = ["time"] + [f"lane {n}" for n in range(0, N)])
        self.lane_df = self.lane_df.set_index("time")
        fig, ax = plt.subplots(1, 1, figsize=(12, 3))
        ax.plot(self.lane_df)
        ax.set_xlabel("time / min")
        ax.set_title("length of checkout lanes")
        ax.legend(self.lane_df.columns)

# create simulation environment
env = simpy.Environment()

# create simulation objects (agents)
customer_generator = CustomerGenerator()
checkouts = [
    Checkout(simpy.Store(env), t_item=1/5),
    Checkout(simpy.Store(env), t_item=1/5),
    Checkout(simpy.Store(env), item_limit=5),
    Checkout(simpy.Store(env)),
    Checkout(simpy.Store(env)),
]
lane_logger = LaneLogger()

# register agents
env.process(customer_generator.process())
for checkout in checkouts:
    env.process(checkout.process())
env.process(lane_logger.process())

# run process
env.run(until=600)

# plot results
lane_logger.report()
lane_logger.plot()
```

```python

```

< [3.9 Refinements of a Grocery Store Checkout Operation](https://jckantor.github.io/CBE40455-2020/03.09-Refinements-to-the-Grocery-Store-Checkout-Operation.html) | [Contents](toc.html) | [3.11 Batch Chemical Process](https://jckantor.github.io/CBE40455-2020/03.11-Project-Batch-Chemical-Process.html) >


# Introducción a Python

## introducción

[Python](https://www.python.org/) es un lenguaje de propósito general sencillo y expresivo. Permite acceder cómodamente a una amplia colección de bibliotecas útiles en todos los campos de la informática. Su uso en ciencia y tecnología es cada vez mayor.

Es un lenguaje interpretado, con tipos de datos dinámicos y manejo automático de memoria, que puede utilizarse tanto para escribir programas de forma tradicional como para experimentar en un entorno interactivo. Incluye las construcciones más importantes de programación funcional y admite programación orientada a objetos.

La sintaxis es simple e intuitiva pero hay que tener en cuenta algunas características:

- Los bloques de instrucciones de las construcciones condicionales, bucles y funciones se delimitan mediante la "indentación del código": no se utiliza "end" ni `{` `}`.

- Los índices para acceder a los arrays o listas comienzan en 0 y acaban en tamaño-1. Las secuencias (*range*) usadas en bucles o *list comprehensions* no incluyen el límite superior.

- Algunas funciones tienen la sintaxis tradicional `f(x)`, `g(x,a)`, mientras que otras se expresan como `x.f()`, `x.g(a)`, etc., indicando que el "objeto" `x` se modifica de alguna forma.

- Los arrays y las listas son "mutables": su asignación a otra variable **no** crea una copia del objeto original sino una "referencia" a través de la cual se puede modificar la estructura original.

- Las funciones pueden leer directamente el valor de variables globales, pero para modificarlas hay que declararlas como `global`. La asignación de variables dentro de una función crea variables locales.

- La única forma de crear un ámbito de variables es definir una función. Los índices de los bucles son visibles a la salida.

### instalación

[Anaconda](https://www.continuum.io/DOWNLOADS)

Si partimos de la instalación mínima [miniconda](https://conda.io/miniconda.html) necesitamos los siguiente paquetes:

    > conda install jupyter numpy scipy sympy matplotlib

Arrancamos el "servidor" de notebooks con

    > jupyter notebook

Las celdas de código se evalúan pulsando Mayúsculas-Entrar.

## tipos simples

Cadenas de caracteres:

```python
s = 'Hola'
```

```python
s
```

    'Hola'

```python
print(s)
```

    Hola

```python
type(s)
```

    str

Se admiten diferentes tipos de delimitadores y cadenas multilínea.

```python
"Hola" + ''' amigos!'''
```

    'Hola amigos!'

Variables lógicas:

```python
c = 3 < 4
```

```python
type(c)
```

    bool

```python
c and (2==1+1) or not (3 != 5)
```

    True

Números reales aproximados con coma flotante de doble precisión:

```python
x = 3.5
```

```python
type(x)
```

    float

Los enteros tienen tamaño ilimitado:

```python
x = 20
```

```python
type(x)
```

    int

```python
x**x
```

    104857600000000000000000000

Variable compleja:

```python
(1+1j)*(1-1j)
```

    (2+0j)

```python
import cmath

cmath.sqrt(-1)
```

    1j

## control

Condiciones:

```python
k = 7

if k%2 == 0:
    print(k," es par")
else:
    print(k," es impar")
    print("me gustan los impares")
```

    7  es impar
    me gustan los impares

Bucles:

```python
for k in [1,2,3]:
    print(k)
```

    1
    2
    3

```python
for k in range(5):
    print(k)
```

    0
    1
    2
    3
    4

```python
k = 1
p = 1
while k < 5:
    p = p*k
    k = k+1
p
```

    24

## contenedores

### tuplas

```python
t = (2,'rojo')
```

```python
t
```

    (2, 'rojo')

```python
t[0]
```

    2

Son inmutables.

### listas

```python
l = [1,-2,67,0,8,1,3]
```

```python
type(l)
```

    list

También admite elementos de diferentes tipos, incluyendo otras listas, tuplas, o cualquier otro tipo de datos, aunque lo normal es trabajar con listas homogéneas (con elementos del mismo tipo) cuyos elementos pueden procesarse todos de la misma manera usando un bucle.

La extracción de elementos ("indexado"), la longitud de la lista y la suma de sus elementos se consiguen exactamente igual que en las tuplas:

```python
l[2], len(l), sum(l)
```

    (67, 7, 78)

Sin embargo, las listas se diferencian en una característica fundamental. Son **mutables**: podemos añadir o quitar elementos de ellas.

```python
l.append(28)

l
```

    [1, -2, 67, 0, 8, 1, 3, 28]

```python
l += [-2,4]

l
```

    [1, -2, 67, 0, 8, 1, 3, 28, -2, 4]

```python
l.remove(0)

l
```

    [1, -2, 67, 8, 1, 3, 28, -2, 4]

```python
l[2] = 7

l
```

    [1, -2, 7, 8, 1, 3, 28, -2, 4]

```python
l.pop()
```

    4

```python
l
```

    [1, -2, 7, 8, 1, 3, 28, -2]

```python
del l[2]
```

```python
l
```

    [1, -2, 8, 1, 3, 28, -2]

```python
l.insert(3,100)

l
```

    [1, -2, 8, 100, 1, 3, 28, -2]

### conjuntos

El tipo `set` trata de reproducir el concepto matemático de conjunto. Se construye con llaves y los elementos duplicados se eliminan automáticamente.

```python
C = {1,2,7,1,8,2,1}
C
```

    {1, 2, 7, 8}

Las operaciones de conjuntos están disponibles con símbolos o con "métodos" (funciones en forma de sufijo) . Los detalles pueden encontrarse en la [documentación](https://docs.python.org/3.6/library/stdtypes.html?highlight=set#set).

```python
C.union({0,8})
```

    {0, 1, 2, 7, 8}

```python
C | {0,8}
```

    {0, 1, 2, 7, 8}

```python
C & {5,2}
```

    {2}

```python
C - {2,8,0,5}
```

    {1, 7}

```python
5 in C
```

    False

```python
{1,2} < {5,2,1}
```

    True

### diccionarios

Es un array asociativo (el índice puede ser cualquier tipo (inmutable)). Es una estructura muy utilizada en Python.

```python
d = {'lunes': 8, 'martes' : [1,2,3], 3: 5}
```

```python
d['martes']
```

    [1, 2, 3]

```python
d.keys()
```

    dict_keys(['lunes', 'martes', 3])

```python
d.values()
```

    dict_values([8, [1, 2, 3], 5])

### iteración en contenedores

Si queremos procesar todos los elementos de un contenedor podemos hacer un bucle y acceder a cada uno de ellos con la operación de indexado.

```python
lista = [1,2,3,4,5]

for k in range(len(lista)):
    print(lista[k])
```

    1
    2
    3
    4
    5

Esta construcción es tan común que en Python podemos escribirla de forma mucho más natural:

```python
for x in lista:
    print(x)
```

    1
    2
    3
    4
    5

Esto funciona incluso en contenedores como `set` que no admiten el indexado. Los tipos contenedores se pueden "recorrer" directamente mediante un bucle `for`, visitando todos sus elementos.

### conversión

El nombre de un contenedor es a la vez una función para construir un contenedor de ese tipo a partir de otro contenedor cualquiera.

```python
l = [4,2,2,3,3,3,3,1]

tuple(l)
```

    (4, 2, 2, 3, 3, 3, 3, 1)

```python
set(l)
```

    {1, 2, 3, 4}

```python
list({5,4,3})
```

    [3, 4, 5]

```python
list(range(10))
```

    [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

Esta característica funciona con cualquier otro tipo, no solo con contenedores:

```python
float(5)
```

    5.0

```python
int('54')
```

    54

Si la conversión no es posible se producirá un error.

### subsecuencias

```python
l = list(range(20))

l
```

    [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19]

```python
l[:5]
```

    [0, 1, 2, 3, 4]

```python
l[4:]
```

    [4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19]

```python
l[-3:]
```

    [17, 18, 19]

```python
l[5:10:2]
```

    [5, 7, 9]

```python
l[::-1]
```

    [19, 18, 17, 16, 15, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1, 0]

```python
l[10:14] = [0,0,0]

l
```

    [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 0, 0, 14, 15, 16, 17, 18, 19]

### *list comprehensions*

Cuando utilizamos un bucle para recorrer una lista o realizar un gran número de cálculos los resultados intermedios se pueden imprimir si se desea, pero en cualquier caso al final se pierden.

Muchas veces surge la necesidad de construir una lista (o cualquier otro tipo de contenedor) a partir de los elementos de otra. Una forma de programarlo es empezar con una lista vacía e iterar mediante un bucle añadiendo elementos.

Supongamos que queremos construir una lista con los 100 primeros números cuadrados $1,4,9,16,\ldots,10000$. En principio parece razonable hacer lo siguiente:

```python
r = []
for k in range(1,101):
    r.append(k**2)

print(r)
```

    [1, 4, 9, 16, 25, 36, 49, 64, 81, 100, 121, 144, 169, 196, 225, 256, 289, 324, 361, 400, 441, 484, 529, 576, 625, 676, 729, 784, 841, 900, 961, 1024, 1089, 1156, 1225, 1296, 1369, 1444, 1521, 1600, 1681, 1764, 1849, 1936, 2025, 2116, 2209, 2304, 2401, 2500, 2601, 2704, 2809, 2916, 3025, 3136, 3249, 3364, 3481, 3600, 3721, 3844, 3969, 4096, 4225, 4356, 4489, 4624, 4761, 4900, 5041, 5184, 5329, 5476, 5625, 5776, 5929, 6084, 6241, 6400, 6561, 6724, 6889, 7056, 7225, 7396, 7569, 7744, 7921, 8100, 8281, 8464, 8649, 8836, 9025, 9216, 9409, 9604, 9801, 10000]

No está mal, pero los lenguajes modernos proporcionan una herramienta mucho más elegante para expresar este tipo de cálculos. Se conoce como [list comprehension](https://en.wikipedia.org/wiki/List_comprehension) y trata de imitar la notación matemática para definir conjuntos:

$$ r = \{ k^2 \; : \; \forall k \in \mathbb{N},  \;1 \leq k \leq 100 \} $$

```python
r = [ k**2 for k in range(1,101) ]

print(r)
```

    [1, 4, 9, 16, 25, 36, 49, 64, 81, 100, 121, 144, 169, 196, 225, 256, 289, 324, 361, 400, 441, 484, 529, 576, 625, 676, 729, 784, 841, 900, 961, 1024, 1089, 1156, 1225, 1296, 1369, 1444, 1521, 1600, 1681, 1764, 1849, 1936, 2025, 2116, 2209, 2304, 2401, 2500, 2601, 2704, 2809, 2916, 3025, 3136, 3249, 3364, 3481, 3600, 3721, 3844, 3969, 4096, 4225, 4356, 4489, 4624, 4761, 4900, 5041, 5184, 5329, 5476, 5625, 5776, 5929, 6084, 6241, 6400, 6561, 6724, 6889, 7056, 7225, 7396, 7569, 7744, 7921, 8100, 8281, 8464, 8649, 8836, 9025, 9216, 9409, 9604, 9801, 10000]

```python
[ k for k in range(100) if k%7 == 0 ]
```

    [0, 7, 14, 21, 28, 35, 42, 49, 56, 63, 70, 77, 84, 91, 98]

```python
[(a,b) for a in range(1,7) for b in range(1,7) if a + b >= 10 ]
```

    [(4, 6), (5, 5), (5, 6), (6, 4), (6, 5), (6, 6)]

```python
{ a+b for a in range(1,7) for b in range(1,7) }
```

    {2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12}

```python
sum([k**2 for k in range(100+1)])
```

    338350

### desestructuración

En Python es posible asignar nombres a los elementos de una secuencia de forma muy natural.

Supongamos que tenemos una tupla como la siguiente

```python
t = (3,4,5)
```

y queremos operar con sus elementos. Podemos acceder con un índice:

```python
t[1] + t[2]
```

    9

No hay ningún problema pero el acceso con índice se hace pesado si los elementos aparecen varias veces en el código. En estos casos es mejor ponerles nombre. Podemos hacer

```python
b = t[1]
c = t[2]

b+c
```

    9

Sin embargo Python nos permite algo más elegante:

```python
_,b,c = t

b+c
```

    9

(El nombre `_`  se suele usar cuando no necesitamos ese elemento.)

Usando esta característica podemos escribir varias asignaciones de una vez:

```python
x,y = 23,45
```

Un nombre con asterisco captura dentro de una lista todos los elementos restantes:

```python
s = 'Alberto'

x, y, *z, w = s
```

```python
y
```

    'l'

```python
z
```

    ['b', 'e', 'r', 't']

La desestructuración de argumentos es muy práctica en combinación con las *list comprehensions*:

```python
l = [(k,k**2) for k in range(5)]
l
```

    [(0, 0), (1, 1), (2, 4), (3, 9), (4, 16)]

```python
[a+b for a,b in l]
```

    [0, 2, 6, 12, 20]

## funciones

```python
def sp(n):
    r = n**2+n+41
    return r
```

```python
sp(5)
```

    71

Se pueden devolver varios resultados en una tupla:

```python
import math

def ecsec(a,b,c):
    d = math.sqrt(b**2- 4*a*c)
    s1 = (-b+d)/2/a
    s2 = (-b-d)/2/a
    return (s1,s2)
```

```python
ecsec(2,-6,4)
```

    (2.0, 1.0)

Los paréntesis de la tupla son opcionales.

```python
a,b = ecsec(1,-3,2)

b
```

    1.0

Las variables globales son visibles dentro de las funciones y las asignaciones crean variables locales (a menos que el nombre se declare `global`).

```python
a = 5

b = 8

def f(x):
    b = a+1
    return b

print(f(3))
print(b)
```

    6
    8

```python
a = 5

b = 8

def f(x):
    global b
    b = a+1
    return b

print(f(3))
print(b)
```

    6
    6

Argumentos por omisión:

```python
def incre(x,y=1):
    return x + y

print(incre(5))
print(incre(5,3))
```

    6
    8

Argumentos por nombre:

```python
incre(y=3, x=2)
```

    5

Documentación:

```python
# ? sum
help(sum)
```

    Help on built-in function sum in module builtins:

    sum(iterable, start=0, /)
        Return the sum of a 'start' value (default: 0) plus an iterable of numbers

        When the iterable is empty, return the start value.
        This function is intended specifically for use with numeric values and may
        reject non-numeric types.

```python
def fun(n):
    """Una función muy simple que calcula el triple de su argumento."""
    return 3*n
```

```python
help(fun)
```

    Help on function fun in module __main__:

    fun(n)
        Una función muy simple que calcula el triple de su argumento.

### bibliotecas

Las funciones definidas en un archivo se pueden utilizar directamente haciendo un `import`. Existe una convención para definir una función `main` que se ejecuta cuando el archivo se arranca como programa y suele usarse para ejecutar tests.

### programación funcional

En Python 3 las construcciones funcionales crean secuencias "bajo demanda".

```python
map(sp,range(5))
```

    <map at 0x7f343087d240>

```python
for k in map(sp,range(5)):
    print(k)
```

    41
    43
    47
    53
    61

```python
list(map(sp,range(5)))
```

    [41, 43, 47, 53, 61]

```python
list(filter(lambda x: x%2 == 1, range(10)))
```

    [1, 3, 5, 7, 9]

Es poco frecuente usar explícitamente map y filter, ya que su efecto se consigue de forma más cómoda con list comprehensions:

```python
[k**2 for k in range(10) if k >5 ]
```

    [36, 49, 64, 81]

```python
def divis(n):
    return [k for k in range(2,n) if n%k==0]
```

```python
divis(12)
```

    [2, 3, 4, 6]

```python
divis(1001)
```

    [7, 11, 13, 77, 91, 143]

```python
def perfect(n):
    return sum(divis(n)) + 1 == n
```

```python
perfect(4)
```

    False

```python
perfect(6)
```

    True

```python
def prime(n):
    return divis(n)==[]
```

```python
[k for k in range(2,21) if prime(k)]
```

    [2, 3, 5, 7, 11, 13, 17, 19]

```python
from functools import reduce
import operator

def product(l):
    return reduce(operator.mul,l,1)
```

```python
product(range(1,10+1))
```

    3628800

Función que construye funciones:

```python
def mkfun(y):
    return lambda x: x+y
```

```python
f = mkfun(1)
g = mkfun(5)

print(f(10))
print(g(10))
```

    11
    15

```python
fs = list(map(mkfun,range(1,6)))

print(fs[0](10))
print(fs[4](10))
```

    11
    15

## arrays

Gran parte del éxito de Python se debe a [numpy](http://www.numpy.org/).

```python
import numpy as np
```

Construcción a partir de listas (u otros contenedores):

```python
m = np.array([[5,3, 2,10],
              [2,0, 7, 0],
              [1,1,-3, 6]])
```

```python
m[1,2]
```

    7

Inspección de su tipo y estructura:

```python
type(m)
```

    numpy.ndarray

```python
m.dtype
```

    dtype('int64')

```python
m.shape
```

    (3, 4)

```python
m.ndim
```

    2

```python
len(m)
```

    3

```python
m.size
```

    12

Las operaciones elemento a elemento son automáticas:

```python
5*m + 2
```

    array([[ 27,  17,  12,  52],
           [ 12,   2,  37,   2],
           [  7,   7, -13,  32]])

Constructores especiales:

```python
np.zeros([2,3])
```

    array([[0., 0., 0.],
           [0., 0., 0.]])

```python
np.ones([4])
```

    array([1., 1., 1., 1.])

```python
np.linspace(0,5,11)
```

    array([0. , 0.5, 1. , 1.5, 2. , 2.5, 3. , 3.5, 4. , 4.5, 5. ])

```python
np.arange(10)
```

    array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])

```python
np.arange(1,10,0.5)
```

    array([1. , 1.5, 2. , 2.5, 3. , 3.5, 4. , 4.5, 5. , 5.5, 6. , 6.5, 7. ,
           7.5, 8. , 8.5, 9. , 9.5])

```python
np.eye(7)
```

    array([[1., 0., 0., 0., 0., 0., 0.],
           [0., 1., 0., 0., 0., 0., 0.],
           [0., 0., 1., 0., 0., 0., 0.],
           [0., 0., 0., 1., 0., 0., 0.],
           [0., 0., 0., 0., 1., 0., 0.],
           [0., 0., 0., 0., 0., 1., 0.],
           [0., 0., 0., 0., 0., 0., 1.]])

Iteración, a lo largo de la primera dimensión:

```python
for e in np.arange(4):
    print(e)
```

    0
    1
    2
    3

```python
for e in m:
    print(e)
```

    [ 5  3  2 10]
    [2 0 7 0]
    [ 1  1 -3  6]

```python
sum(m)
```

    array([ 8,  4,  6, 16])

```python
np.sum(m,axis=1)
```

    array([20,  9,  5])

Operaciones matriciales:

```python
m.T
```

    array([[ 5,  2,  1],
           [ 3,  0,  1],
           [ 2,  7, -3],
           [10,  0,  6]])

```python
v = np.array([3,2,-5,8])
```

El producto de matrices, el producto escalar de vectores, y su generalización para arrays multidimensionales se expresa con el símbolo `@` (que representa a la función `dot`).

```python
m @ v
```

    array([ 91, -29,  68])

```python
np.diag([10,0,1]) @ m
```

    array([[ 50,  30,  20, 100],
           [  0,   0,   0,   0],
           [  1,   1,  -3,   6]])

Las funciones matemáticas están optimizadas para operar con arrays elemento a elemento:

```python
x = np.linspace(0,2*np.pi,30)

x
```

    array([0.        , 0.21666156, 0.43332312, 0.64998469, 0.86664625,
           1.08330781, 1.29996937, 1.51663094, 1.7332925 , 1.94995406,
           2.16661562, 2.38327719, 2.59993875, 2.81660031, 3.03326187,
           3.24992343, 3.466585  , 3.68324656, 3.89990812, 4.11656968,
           4.33323125, 4.54989281, 4.76655437, 4.98321593, 5.1998775 ,
           5.41653906, 5.63320062, 5.84986218, 6.06652374, 6.28318531])

```python
y = np.sin(x) + np.cos(2*x)
y
```

    array([ 1.        ,  1.12254586,  1.06727539,  0.87270255,  0.60038006,
            0.32232498,  0.10669282,  0.00439546,  0.03917335,  0.20298123,
            0.45755084,  0.74183837,  0.9839623 ,  1.1153946 ,  1.08473957,
            0.86850154,  0.47679154, -0.04714542, -0.63356055, -1.19782715,
           -1.65497221, -1.93447969, -1.99267137, -1.82040717, -1.44469911,
           -0.92394405, -0.33764588,  0.22749718,  0.69260498,  1.        ])

Reconfiguración de los elementos:

```python
np.arange(12)
```

    array([ 0,  1,  2,  3,  4,  5,  6,  7,  8,  9, 10, 11])

```python
np.arange(12).reshape(3,2,2)
```

    array([[[ 0,  1],
            [ 2,  3]],

           [[ 4,  5],
            [ 6,  7]],

           [[ 8,  9],
            [10, 11]]])

### matrices por bloques

```python
np.append(m,[[100,200,300,400],
             [0,  10,  0,  1] ],axis=0)
```

    array([[  5,   3,   2,  10],
           [  2,   0,   7,   0],
           [  1,   1,  -3,   6],
           [100, 200, 300, 400],
           [  0,  10,   0,   1]])

```python
np.hstack([np.zeros([3,3]),np.ones([3,2])])
```

    array([[0., 0., 0., 1., 1.],
           [0., 0., 0., 1., 1.],
           [0., 0., 0., 1., 1.]])

```python
np.vstack([np.eye(3),5*np.ones([2,3])])
```

    array([[1., 0., 0.],
           [0., 1., 0.],
           [0., 0., 1.],
           [5., 5., 5.],
           [5., 5., 5.]])

numpy proporciona un tipo especial `matrix` para los arrays de 2 dimensiones pero [se recomienda no usarlo](https://stackoverflow.com/questions/4151128/what-are-the-differences-between-numpy-arrays-and-matrices-which-one-should-i-u) o usarlo con cuidado.

### automatic broadcasting

Las operaciones elemento a elemento argumentos con las mismas dimensiones. Pero si alguna dimensión es igual a uno, se sobreentiende que los elementos se replican en esa dimensión para coincidir con el otro array.

```python
m = np.array([[1, 2, 3, 4]
             ,[5, 6, 7, 8]
             ,[9,10,11,12]])
```

```python
m + [[10],
     [20],
     [30]]
```

    array([[11, 12, 13, 14],
           [25, 26, 27, 28],
           [39, 40, 41, 42]])

```python
m + [100,200,300,400]
```

    array([[101, 202, 303, 404],
           [105, 206, 307, 408],
           [109, 210, 311, 412]])

```python
np.array([[1,2,3,4]])   + np.array([[100],
                                    [200],
                                    [300]])
```

    array([[101, 102, 103, 104],
           [201, 202, 203, 204],
           [301, 302, 303, 304]])

### slices

Extracción de elementos y "submatrices" o "subarrays", seleccionando intervalos de filas, columnas, etc.:

```python
m = np.arange(42).reshape(6,7)
m
```

    array([[ 0,  1,  2,  3,  4,  5,  6],
           [ 7,  8,  9, 10, 11, 12, 13],
           [14, 15, 16, 17, 18, 19, 20],
           [21, 22, 23, 24, 25, 26, 27],
           [28, 29, 30, 31, 32, 33, 34],
           [35, 36, 37, 38, 39, 40, 41]])

```python
m[1,2]
```

    9

```python
m[2:5,1:4]
```

    array([[15, 16, 17],
           [22, 23, 24],
           [29, 30, 31]])

```python
m[:3, 4:]
```

    array([[ 4,  5,  6],
           [11, 12, 13],
           [18, 19, 20]])

```python
m[[1,0,0,2,1],:]
```

    array([[ 7,  8,  9, 10, 11, 12, 13],
           [ 0,  1,  2,  3,  4,  5,  6],
           [ 0,  1,  2,  3,  4,  5,  6],
           [14, 15, 16, 17, 18, 19, 20],
           [ 7,  8,  9, 10, 11, 12, 13]])

Los índices negativos indican que se empieza a contar desde el final.

```python
# las dos últimas columnas y todas las filas menos las tres últimas.
m[:-3,-2:]
```

    array([[ 5,  6],
           [12, 13],
           [19, 20]])

```python
# la penúltima columna
m[:,-2]
```

    array([ 5, 12, 19, 26, 33, 40])

```python
# la penúltima columna pero como array 2D (matriz), para que se vea como un vector columna
m[:,[-2]]
```

    array([[ 5],
           [12],
           [19],
           [26],
           [33],
           [40]])

### masks

Extracción de elementos que cumplen una condición:

```python
n = np.arange(10)

n
```

    array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])

```python
n < 5
```

    array([ True,  True,  True,  True,  True, False, False, False, False,
           False])

```python
n[n<5]
```

    array([0, 1, 2, 3, 4])

```python
k = np.arange(1,101)

(k ** 2)[(k>10) & (k**3 < 2000)]
```

    array([121, 144])

### I/O

La función `np.loadtxt` permite cargar los datos de los arrays a partir de ficheros de texto. También es posible guardar y recuperar arrays en formato binario.

## gráficas

Uno de los paquetes gráficos más conocidos es `matplotlib`, que puede utilizarse con un interfaz muy parecido al de Matlab/Octave.

```python
import numpy as np
import matplotlib.pyplot as plt

# para insertar los gráficos en el notebook
%matplotlib inline

# para generar ventanas independientes
# %matplotlib qt
# %matplotib  tk
```

```python
x=np.linspace(0,2*np.pi,200)
```

```python
plt.plot(np.sin(x))
```

```python
plt.plot(np.cos(x),np.sin(x)); plt.axis('equal');
```

```python
plt.plot(x,np.sin(x), x,np.cos(x));
```

```python
plt.plot(x,np.sin(x),color='red')
plt.plot(x,np.sin(2*x),color='black')
plt.plot([1,2.5],[-0.5,0],'.',markerSize=15);
plt.legend(['hola','fun','puntos']);
plt.xlabel('x'); plt.ylabel('y'); plt.title('bonito plot'); plt.axis('tight');
```

El gráfico se puede exportar en el formato deseado:

```python
# plt.savefig('result.pdf')  # o .svg, .png, .jpg, etc.
```

```python
plt.plot(x,np.exp(x)); plt.axis([0,3,-1,5]);
```

```python
for k in [1,2,3]:
    plt.plot(x,np.sin(k*x))
plt.grid()
```

```python
def espiral(n):
    t = np.linspace(0,n*2*np.pi,1000)
    r = 3 * t
    x = r * np.cos(t)
    y = r * np.sin(t)
    plt.plot(x,y)
    plt.axis('equal')
    plt.axis('off')

espiral(4)
```

```python
import numpy.random as rnd

def randwalk(n,s):
    p = s*rnd.randn(n,2)
    r = np.cumsum(p,axis=0)
    x = r[:,0]
    y = r[:,1]
    plt.plot(x,y)
    plt.axis('equal');
```

```python
plt.figure(figsize=(4,4))
randwalk(1000,1)
```

```python
plt.figure(figsize=(8,8))
x = np.linspace(0,6*np.pi,100);

plt.subplot(2,2,1)
plt.plot(x,np.sin(x),'r')

plt.subplot(2,2,2)
plt.plot(x,np.cos(x))

plt.subplot(2,2,3)
plt.plot(x,np.sin(2*x))

plt.subplot(2,2,4)
plt.plot(x,np.cos(2*x),'g');
```

```python
x,y = np.mgrid[-3:3:0.2,-3:3:0.2]

z = x**2-y**2-1
```

```python
from mpl_toolkits.mplot3d import Axes3D
from matplotlib import cm

fig = plt.figure(figsize=(8,6))
ax = fig.add_subplot(111, projection='3d')

ax.plot_surface(x,y,x**2-y**2, cmap=cm.coolwarm, linewidth=0.5, rstride=2, cstride=2);
```

```python
plt.figure(figsize=(6,6))
plt.contour(x,y, z , colors=['k']);
plt.axis('equal');
```

### animaciones

```python
# si se produce un error:
# conda install -c menpo ffmpeg

from matplotlib import animation, rc
from IPython.display import HTML
rc('animation', html='html5')
```

```python
x = np.linspace(0,2,100)

def wave(lam,freq,x,t):
    return 1*np.sin(2*np.pi*(x/lam - t*freq))
```

```python
fig, ax = plt.subplots()
plt.grid()
plt.title('onda viajera')
plt.xlabel('x');
plt.close();
ax.set_xlim(( 0, 2))
ax.set_ylim((-1.1, 1.1))

line1, = ax.plot([], [], '-')
#line2, = ax.plot([], [], '.', markerSize=20)

lam  = 0.8
freq = 1/4

def animate(i):
    t = i/25
    line1.set_data(x,wave(lam,freq,x,t))
    #line2.set_data(1,f(lam,freq,1,t))
    return ()

animation.FuncAnimation(fig, animate, frames=100, interval=1000/25, blit=True)
```

### data frames

El módulo `pandas` proporciona el tipo "dataframe", muy utilizado en análisis de datos. Permite leer conjuntos de datos almacenados en archivos que pueden estar incluso en una máquina remota.

```python
import pandas as pd

df = pd.read_table('https://robot.inf.um.es/material/data/ConstanteHubbleDatos-1.txt', sep='\s+', comment='#')
df
```

<div>
<style scoped>
    .dataframe tbody tr th:only-of-type {
        vertical-align: middle;
    }

    .dataframe tbody tr th {
        vertical-align: top;
    }

    .dataframe thead th {
        text-align: right;
    }
</style>
<table border="1" class="dataframe">
  <thead>
    <tr style="text-align: right;">
      <th></th>
      <th>V(km/s)</th>
      <th>Redshift</th>
      <th>Magnitud</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <th>0</th>
      <td>18287</td>
      <td>0.060998</td>
      <td>17.62</td>
    </tr>
    <tr>
      <th>1</th>
      <td>5691</td>
      <td>0.018983</td>
      <td>15.00</td>
    </tr>
    <tr>
      <th>2</th>
      <td>26382</td>
      <td>0.088000</td>
      <td>18.59</td>
    </tr>
    <tr>
      <th>3</th>
      <td>5996</td>
      <td>0.020000</td>
      <td>15.54</td>
    </tr>
    <tr>
      <th>4</th>
      <td>19202</td>
      <td>0.064051</td>
      <td>15.30</td>
    </tr>
    <tr>
      <th>5</th>
      <td>23684</td>
      <td>0.079000</td>
      <td>16.56</td>
    </tr>
    <tr>
      <th>6</th>
      <td>11702</td>
      <td>0.039034</td>
      <td>17.14</td>
    </tr>
    <tr>
      <th>7</th>
      <td>17284</td>
      <td>0.057653</td>
      <td>13.50</td>
    </tr>
    <tr>
      <th>8</th>
      <td>13491</td>
      <td>0.045000</td>
      <td>17.80</td>
    </tr>
    <tr>
      <th>9</th>
      <td>10566</td>
      <td>0.035244</td>
      <td>15.25</td>
    </tr>
    <tr>
      <th>10</th>
      <td>14718</td>
      <td>0.049094</td>
      <td>15.60</td>
    </tr>
    <tr>
      <th>11</th>
      <td>13491</td>
      <td>0.045000</td>
      <td>14.52</td>
    </tr>
    <tr>
      <th>12</th>
      <td>16325</td>
      <td>0.054453</td>
      <td>15.30</td>
    </tr>
    <tr>
      <th>13</th>
      <td>20686</td>
      <td>0.069000</td>
      <td>16.80</td>
    </tr>
    <tr>
      <th>14</th>
      <td>1808</td>
      <td>0.006031</td>
      <td>11.16</td>
    </tr>
    <tr>
      <th>15</th>
      <td>7603</td>
      <td>0.025361</td>
      <td>15.18</td>
    </tr>
    <tr>
      <th>16</th>
      <td>1018</td>
      <td>0.003395</td>
      <td>12.24</td>
    </tr>
    <tr>
      <th>17</th>
      <td>321</td>
      <td>0.001071</td>
      <td>13.00</td>
    </tr>
    <tr>
      <th>18</th>
      <td>3106</td>
      <td>0.010360</td>
      <td>12.49</td>
    </tr>
    <tr>
      <th>19</th>
      <td>9426</td>
      <td>0.031442</td>
      <td>14.53</td>
    </tr>
    <tr>
      <th>20</th>
      <td>7464</td>
      <td>0.024897</td>
      <td>15.21</td>
    </tr>
    <tr>
      <th>21</th>
      <td>15143</td>
      <td>0.050512</td>
      <td>17.40</td>
    </tr>
    <tr>
      <th>22</th>
      <td>407</td>
      <td>0.001358</td>
      <td>10.87</td>
    </tr>
    <tr>
      <th>23</th>
      <td>7257</td>
      <td>0.024207</td>
      <td>14.60</td>
    </tr>
    <tr>
      <th>24</th>
      <td>9193</td>
      <td>0.030664</td>
      <td>15.10</td>
    </tr>
    <tr>
      <th>25</th>
      <td>12137</td>
      <td>0.040485</td>
      <td>14.75</td>
    </tr>
    <tr>
      <th>26</th>
      <td>4264</td>
      <td>0.014224</td>
      <td>14.98</td>
    </tr>
    <tr>
      <th>27</th>
      <td>4381</td>
      <td>0.014615</td>
      <td>14.15</td>
    </tr>
    <tr>
      <th>28</th>
      <td>22484</td>
      <td>0.075000</td>
      <td>17.43</td>
    </tr>
    <tr>
      <th>29</th>
      <td>15162</td>
      <td>0.050575</td>
      <td>16.50</td>
    </tr>
    <tr>
      <th>30</th>
      <td>30000</td>
      <td>0.101000</td>
      <td>18.90</td>
    </tr>
    <tr>
      <th>31</th>
      <td>12981</td>
      <td>0.043300</td>
      <td>15.23</td>
    </tr>
    <tr>
      <th>32</th>
      <td>8803</td>
      <td>0.029364</td>
      <td>14.90</td>
    </tr>
  </tbody>
</table>
</div>

Puede convertirse en un array normal:

```python
A = np.array(df)
A
```

    array([[1.8287e+04, 6.0998e-02, 1.7620e+01],
           [5.6910e+03, 1.8983e-02, 1.5000e+01],
           [2.6382e+04, 8.8000e-02, 1.8590e+01],
           [5.9960e+03, 2.0000e-02, 1.5540e+01],
           [1.9202e+04, 6.4051e-02, 1.5300e+01],
           [2.3684e+04, 7.9000e-02, 1.6560e+01],
           [1.1702e+04, 3.9034e-02, 1.7140e+01],
           [1.7284e+04, 5.7653e-02, 1.3500e+01],
           [1.3491e+04, 4.5000e-02, 1.7800e+01],
           [1.0566e+04, 3.5244e-02, 1.5250e+01],
           [1.4718e+04, 4.9094e-02, 1.5600e+01],
           [1.3491e+04, 4.5000e-02, 1.4520e+01],
           [1.6325e+04, 5.4453e-02, 1.5300e+01],
           [2.0686e+04, 6.9000e-02, 1.6800e+01],
           [1.8080e+03, 6.0310e-03, 1.1160e+01],
           [7.6030e+03, 2.5361e-02, 1.5180e+01],
           [1.0180e+03, 3.3950e-03, 1.2240e+01],
           [3.2100e+02, 1.0710e-03, 1.3000e+01],
           [3.1060e+03, 1.0360e-02, 1.2490e+01],
           [9.4260e+03, 3.1442e-02, 1.4530e+01],
           [7.4640e+03, 2.4897e-02, 1.5210e+01],
           [1.5143e+04, 5.0512e-02, 1.7400e+01],
           [4.0700e+02, 1.3580e-03, 1.0870e+01],
           [7.2570e+03, 2.4207e-02, 1.4600e+01],
           [9.1930e+03, 3.0664e-02, 1.5100e+01],
           [1.2137e+04, 4.0485e-02, 1.4750e+01],
           [4.2640e+03, 1.4224e-02, 1.4980e+01],
           [4.3810e+03, 1.4615e-02, 1.4150e+01],
           [2.2484e+04, 7.5000e-02, 1.7430e+01],
           [1.5162e+04, 5.0575e-02, 1.6500e+01],
           [3.0000e+04, 1.0100e-01, 1.8900e+01],
           [1.2981e+04, 4.3300e-02, 1.5230e+01],
           [8.8030e+03, 2.9364e-02, 1.4900e+01]])

```python
x = A[:,0]
y = A[:,2]

# x,_,y = A.T

plt.plot(x,y,'.');
```

## cálculo científico

### números pseudoaleatorios y estadística elemental

`numpy` permite generar arrays de números pseudoaleatorios con diferentes tipos de distribuciones (uniforme, normal, etc.).

Tiene también funciones de estadística descriptiva para calcular características de conjuntos de datos tales como la media, mediana, desviación típica, máximo y mínimo, etc.

Como ejemplo, podemos estudiar la distribución de puntuaciones al lanzar 3 dados.

```python
dados = np.random.randint(1,6+1,(100,3))
dados[:10]
```

    array([[5, 3, 3],
           [3, 2, 6],
           [3, 6, 3],
           [4, 5, 6],
           [2, 1, 3],
           [5, 6, 2],
           [2, 6, 2],
           [3, 4, 1],
           [3, 3, 2],
           [5, 3, 2]])

```python
s = np.sum(dados,axis=1)
s
```

    array([11, 11, 12, 15,  6, 13, 10,  8,  8, 10, 10, 17,  7, 10, 12,  9, 11,
           14, 10, 11, 15, 10,  6,  9,  9, 11,  9, 11, 15, 12,  5,  9, 11, 10,
           10,  7,  8, 11,  9,  9,  8, 11,  8,  9, 12, 10, 10,  7, 14,  7,  5,
            7, 14, 12,  7, 12,  7,  8, 13, 15, 17, 12, 10, 12, 13, 12,  9,  8,
           13, 10,  9, 12, 16, 14, 16,  7,  8,  8, 13, 16, 12, 13, 13,  8,  9,
            9, 10, 15,  9,  8, 15, 11,  8, 12, 10,  3,  7,  6, 10, 13])

```python
plt.hist(s,bins=np.arange(2,19)+0.5);
```

```python
s.mean()
```

    10.43

```python
s.std()
```

    2.8644545728637416

### implementación eficiente

Las operaciones de `numpy` están "optimizadas" (escritas internamente en código C eficiente).

```python
x = np.random.rand(10**8)
```

```python
x
```

    array([0.98545712, 0.76934582, 0.89515805, ..., 0.99684131, 0.91799724,
           0.0971656 ])

```python
%%time

np.mean(x)
```

    CPU times: user 88 ms, sys: 0 ns, total: 88 ms
    Wall time: 88.3 ms

    0.5000571511372741

```python
%%timeit

np.mean(x)
```

    86.8 ms ± 741 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)

```python
%%timeit

x @ x
```

    61.5 ms ± 202 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)

Si la misma operación se realiza "manualmente" con instrucciones normales de Python requiere mucho más tiempo:

```python
%%time

s = 0
for e in x:
    s += e
print(s/len(x))
```

    0.5000571511372518
    CPU times: user 29.2 s, sys: 12 ms, total: 29.2 s
    Wall time: 29.1 s

Por tanto, si usamos los módulos apropiados los programas en Python no tienen por qué ser más lentos que los de otros lenguajes de programación. Python es "glue code", un pegamento para combinar bibliotecas de funciones, escritas en cualquier lenguaje, que resuelven eficientemente problemas específicos.

### álgebra lineal

El submódulo `linalg` ofrece las operaciones usuales de álgebra lineal.

```python
import scipy.linalg as la
```

Por ejemplo, podemos calcular fácilmente el módulo de un vector:

```python
la.norm([1,2,3,4,5])
```

    7.416198487095663

o el determinante de una matriz:

```python
la.det([[1,2],
        [3,4]])
```

    -2.0

Observa que muchas de las funciones que trabajan con arrays admiten también otros contenedores como listas o tuplas, que son transformadas automáticamente en arrays.

Un problema muy importante es la resolución de sistemas de ecuaciones lineales. Si tenemos que resolver un sistema como

$$
\begin{align*}
x + 2y &= 3\\
3x+4y  &= 5
\end{align*}
$$

Lo expresamos en forma matricial $AX=B$ y podemos resolverlo con la inversa de $A$, o directamente con `solve`.

```python
m = np.array([[1,2],
              [3,4]])
```

```python
m
```

    array([[1, 2],
           [3, 4]])

```python
la.inv(m)
```

    array([[-2. ,  1. ],
           [ 1.5, -0.5]])

```python
la.inv(m) @ np.array([3,5])
```

    array([-1.,  2.])

Es mejor (más eficiente y numéricamente estable) usar la función `solve`:

```python
la.solve(m,[3,5])
```

    array([-1.,  2.])

La solución se debería mostrar como una columna, pero en Python los arrays de una dimensión se imprimen como una fila porque no siempre representan vectores matemáticos. Si lo preferimos podemos usar matrices de una sola columna.

```python
x = la.solve(m,[[3],
                [5]])

x
```

    array([[-1.],
           [ 2.]])

```python
m @ x
```

    array([[3.],
           [5.]])

Si el lado derecho de la ecuación matricial $A X = B$ es una matriz, la solución $X$ también lo será.

### computación matricial

Python proporciona una [amplia colección](https://docs.scipy.org/doc/scipy/reference/linalg.html) de funciones de álgebra lineal numérica.

```python
la.eigh([[1,2],
         [2,3]])
```

    (array([-0.23606798,  4.23606798]), array([[-0.85065081,  0.52573111],
            [ 0.52573111,  0.85065081]]))

### mínimos cuadrados

Como ejemplo de uso de las herramientas de álgebra lineal realizaremos el ajuste de un modelo polinomial a unas observaciones ficticias. Encontraremos la solución de mínimo error cuadrático a un sistema de ecuaciones sobredeterminado.

En primer lugar generamos unos datos de prueba artificiales que simulan observaciones contaminadas con ruido de una función no lineal.

```python
import matplotlib.pyplot as plt
%matplotlib inline

x = np.linspace(0,2,30)

y = np.sin(x) + 0.05*np.random.randn(x.size)

plt.plot(x,y,'.');
```

Vamos a ajustar un modelo del tipo $y = ax^2 + bx + c$. Los coeficientes desconocidos $a$, $b$ y $c$ se pueden obtener resolviendo un sistema de ecuaciones lineales.

La matriz de coeficientes tiene potencias de $x$ hasta el grado que nos interesa.

```python
A = np.vstack([x**2, x, np.ones(x.size)]).T

A
```

    array([[0.        , 0.        , 1.        ],
           [0.00475624, 0.06896552, 1.        ],
           [0.01902497, 0.13793103, 1.        ],
           [0.04280618, 0.20689655, 1.        ],
           [0.07609988, 0.27586207, 1.        ],
           [0.11890606, 0.34482759, 1.        ],
           [0.17122473, 0.4137931 , 1.        ],
           [0.23305589, 0.48275862, 1.        ],
           [0.30439952, 0.55172414, 1.        ],
           [0.38525565, 0.62068966, 1.        ],
           [0.47562426, 0.68965517, 1.        ],
           [0.57550535, 0.75862069, 1.        ],
           [0.68489893, 0.82758621, 1.        ],
           [0.80380499, 0.89655172, 1.        ],
           [0.93222354, 0.96551724, 1.        ],
           [1.07015458, 1.03448276, 1.        ],
           [1.2175981 , 1.10344828, 1.        ],
           [1.3745541 , 1.17241379, 1.        ],
           [1.54102259, 1.24137931, 1.        ],
           [1.71700357, 1.31034483, 1.        ],
           [1.90249703, 1.37931034, 1.        ],
           [2.09750297, 1.44827586, 1.        ],
           [2.3020214 , 1.51724138, 1.        ],
           [2.51605232, 1.5862069 , 1.        ],
           [2.73959572, 1.65517241, 1.        ],
           [2.97265161, 1.72413793, 1.        ],
           [3.21521998, 1.79310345, 1.        ],
           [3.46730083, 1.86206897, 1.        ],
           [3.72889417, 1.93103448, 1.        ],
           [4.        , 2.        , 1.        ]])

El lado derecho del sistema es directamente el vector con los valores de $y$, la variable independiente del modelo.

```python
B = np.array(y)

B
```

    array([-0.03752051,  0.03269844,  0.15393717,  0.232999  ,  0.24648556,
            0.31061177,  0.39119775,  0.40540727,  0.51922944,  0.59678345,
            0.64238537,  0.69319441,  0.67420081,  0.82066404,  0.81793398,
            0.83714339,  0.94898355,  0.96896918,  0.97055846,  1.00053331,
            0.97931375,  1.02085556,  0.91095985,  0.90903059,  0.99090395,
            0.97729992,  0.87898498,  0.98701068,  0.96368972,  0.88381366])

El sistema que hay que resolver está sobredeterminado: tiene solo tres incógnitas y tantas ecuaciones como observaciones de la función.

$$A \begin{bmatrix}a\\b\\c\end{bmatrix}= B$$

La solución de [mínimo error cuadrático](https://en.wikipedia.org/wiki/Least_squares) para los coeficientes del modelo se obtiene de manera directa:

```python
sol = la.lstsq(A,B)[0]

sol
```

    array([-0.41752543,  1.31882806, -0.06158726])

```python
ye = A @ sol

plt.plot(x,y,'.',x,ye,'r');
```

Se puede experimentar con polinomios de mayor o menor grado.

### solución numérica de ecuaciones no lineales

Resuelve

$$x^4=16$$

```python
import scipy as sci

sci.roots([1,0,0,0,-16])
```

    array([-2.00000000e+00+0.j,  1.11022302e-16+2.j,  1.11022302e-16-2.j,
            2.00000000e+00+0.j])

Resuelve

$$sin(x)+cos(2x)=0$$

```python
import scipy.optimize as opt

opt.fsolve(lambda x: sci.sin(x) + sci.cos(2*x), 0)
```

    array([-0.52359878])

Resuelve

$$
\begin{align*}
x^2 - 3y &= 10\\
sin(x)+y &= 5
\end{align*}
$$

```python
def fun(z):
    x,y = z
    return [ x**2 - 3*y - 10
           , sci.sin(x) + y - 5]

opt.fsolve(fun,[0.1,-0.1])
```

    array([5.2511881 , 5.85832548])

### minimización

Encuentra $(x,y)$ que minimiza $(x-1)^2 + (y-2)^2-x+3y$

```python
def fun(z):
    x,y = z
    return (x-1)**2 + (y-2)**2 - x + 3*y

opt.minimize(fun,[0.1,-0.1])
```

          fun: 2.500000000000014
     hess_inv: array([[ 0.57758622, -0.18103452],
           [-0.18103452,  0.92241375]])
          jac: array([ 0.00000000e+00, -2.38418579e-07])
      message: 'Optimization terminated successfully.'
         nfev: 12
          nit: 2
         njev: 3
       status: 0
      success: True
            x: array([1.49999999, 0.49999988])

### derivación numérica

Calcula una aproximación numérica para $f'(2)$ cuando $f(x) = \sin(2x)*\exp(\cos(x))$

```python
from scipy.misc import derivative

derivative(lambda x: sci.sin(2*x)*sci.exp(sci.cos(x)),2,1E-6)
```

    -0.40836700757052036

```python
(lambda x: (-np.sin(x)*np.sin(2*x) + 2*np.cos(2*x))*np.exp(np.cos(x)))(2)
```

    -0.40836700756782335

### integración numérica

Calcula una aproximación numérica a la integral definida

$$\int_0^1 \frac{4}{1+x^2}dx$$

```python
from scipy.integrate import quad

quad(lambda x: 4/(1+x**2),0,1)
```

    (3.1415926535897936, 3.4878684980086326e-14)

### ecuaciones diferenciales

Resuelve

$$\ddot{x}+0.95x+0.1\dot{x}=0$$

para $x(0)=10$, $\dot{x}(0)=0, t\in[0,20]$

```python
from scipy.integrate import odeint

def xdot(z,t):
    x,v = z
    return [v,-0.95*x-0.1*v]

t = np.linspace(0,20,1000)
r = odeint(xdot,[10,0],t)
# plt.plot(r);
plt.plot(t,r[:,0],t,r[:,1]);
```

```python
plt.plot(r[:,0],r[:,1]);
```

### cálculo simbólico

[sympy](http://www.sympy.org/en/index.html)

```python
import sympy

x = sympy.Symbol('x')
```

```python
sympy.diff( sympy.sin(2*x**3) , x)
```

    6*x**2*cos(2*x**3)

```python
sympy.integrate(1/(1+x))
```

    log(x + 1)

## varios

### vídeos

```python
from IPython.display import YouTubeVideo
```

```python
YouTubeVideo('p7bzE1E5PMY')
```

### xkcd

```python
plt.xkcd()
plt.plot(np.sin(np.linspace(0, 10)))
plt.title('Whoo Hoo!!!');
```

### estilo del notebook

```python
# podemos "tunearlo"
#from IPython.display import HTML
#HTML(open('../css/nb1.css').read())
```


```python
from IPython.display import IFrame
```

### モチベーション
 - 一次元浅水流方程式（Saint-Venant 方程式）を実河川の計算用に拡張した断面平均一次元浅水流方程式の計算方法について改めて整理しました。

### 実河川の一次元計算の独特の難しさ

#### 横断面形状の取り扱い方法
 参照

#### 水深を陽的に扱えない
 断面平均一次元浅水流方程式は次のとおりですが、
 ポイントは、水深、河床高が定義しづらいため、左辺第3項の$\dfrac{\partial H}{\partial x}$を
 $\dfrac{\partial (h+z_b)}{\partial x}$のように水深（圧力項）と河床高（重力項）に分離できないことです。

$$
\begin{align}
    &\frac{\partial Q}{\partial t} + \frac{\partial }{\partial x}\left(\dfrac{\beta Q^2}{A}\right)
    + gA \frac{\partial H}{\partial x} + gAi_e = 0
\end{align}
$$

そのため、特性曲線の考え方が使いづらくなります。一方、ドライベッド（河床勾配あり）はこの式形のほうが解きやすくなります。

#### Δxを任意に取れない
 実河川では縦断方向200m間隔で横断測量を行うこと一般的です。河川地形は複雑で幾何補間が難しいため、測量間隔を数値計算のΔxと使うことが多い。

### 計算スキーム

以下の3スキームを比較する。

1. 不等流計算
2. 不定流計算：コロケート格子 ⇒ Wuさんの方法を採用（詳細は以下を参照）
3. 不定流計算：スタッガード格子 ⇒  細田さんの方法を採用（詳細は以下を参照）

不定流計算のスキームを選ぶ基準は、1). 急流河川でも解けること、2). ロバストであることです。

特に私のような立場では、一つのモデルで何十河川も計算するので、2).ロバスト性は重要になります。

### テスト計算

#### 計算条件

 - 河床高は、平均河床勾配1/400で縦断方向200m間隔で地形データが存在するとしてその地点に-1から1の乱数を加えて実河川を模した河床形状を作った。
 - 河幅は50mで一定とする。
 - 縦断距離は10km
 - 粗度係数は0.03とする。

```python
IFrame('https://computational-sediment-hyd.github.io/S1D-ComputScheme/zb.html',width=400,height=330)
```

####  計算結果

##### 基本ケース

 - 各計算スキームによる計算水位とフルード数と図化しました。
 - 不定流は適当な初期値を与えて定常になるまで計算しています。
 - 参考として平均河床での等流水深も示しています。

```python
IFrame('https://computational-sediment-hyd.github.io/S1D-ComputScheme/fig1.html',width=630,height=700)
```

 拡大図を見ると、

 - 各スキームで最大1m程度の差が生じている。
 - 不等流が最もシャープな水面形でその次は不定流：コロケート格子であり、不定流：スタッガードが最も滑らかになっている。
 - 河床形状に対する水面形の応答が不等流と不定流でずれている。

#### dxを50mに設定：内挿断面
 - 前述の河床形状をもとにdxを200mから50mに変更するように内挿断面を設定した。
 - 水面形の比較を示す。

```python
IFrame('https://computational-sediment-hyd.github.io/S1D-ComputScheme/fig2.html',width=630,height=580)
```

 拡大図を見ると、

 - 各スキームの差はdx=200mの場合と比較して小さくなっている。
 - この値が真値に近いとすると、dx=200mによる再現性は、
     * 不等流は形状は真値に近いが水位が高い。
     * 不定流は平均的な水位は真値に近い。

### ゆるく考察

 - 前述のとおり実河川の計算ではdxを任意にとることが難しいのでdx=200mで計算することを基本に考える。
 - dx=200の不定流でもう少し高精度な（シャープな）水面形を計算したい。
 - 汎用性を考えるとコロケート格子を使いたい。
 - 以下によるとコロケート格子はスタッガード格子に比べて精度が低くなる。
 - しかし、次の論文（森西ら）による定義点を適切に評価することによりその差は無くなると示されている。
 - 今回の計算では、全体的な水面形のずれはほぼ無いため、局所的な運動量のバランスによって水面形の差が生じていると考えられる。
 - 今後のテーマとして、コロケート格子で局所的な保存則を満たしたスキームの開発を考える。(多分陰解法になる気がします。)