# install Pint if necessary

try:
    import pint
except ImportError:
    !pip install pint

# download modsim.py if necessary

from os.path import exists

filename = 'modsim.py'
if not exists(filename):
    from urllib.request import urlretrieve
    url = 'https://raw.githubusercontent.com/AllenDowney/ModSim/main/'
    local, _ = urlretrieve(url+filename, filename)
    print('Downloaded ' + local)

# import functions from modsim

from modsim import *

# import functions from previous notebook

from chap22 import params
from chap22 import make_system
from chap22 import slope_func
from chap22 import event_func

"""
Explanation: Chapter 23
Modeling and Simulation in Python
Copyright 2021 Allen Downey
License: Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International
End of explanation
"""


def range_func(angle, params):
    params = params.set(angle=angle)
    system = make_system(params)
    results, details = run_solve_ivp(system, slope_func,
                                     events=event_func)
    x_dist = results.iloc[-1].x
    print(angle, x_dist)
    return x_dist

"""
Explanation: In the previous chapter we developed a model of the flight of a
baseball, including gravity and a simple version of drag, but neglecting spin, Magnus force, and the dependence of the coefficient of drag on velocity.
In this chapter we apply that model to an optimization problem.
The Manny Ramirez problem
Manny Ramirez is a former member of the Boston Red Sox (an American
baseball team) who was notorious for his relaxed attitude and taste for practical jokes. Our objective in this chapter is to solve the following Manny-inspired problem:

What is the minimum effort required to hit a home run in Fenway Park?

Fenway Park is a baseball stadium in Boston, Massachusetts. One of its
most famous features is the "Green Monster", which is a wall in left
field that is unusually close to home plate, only 310 feet away. To
compensate for the short distance, the wall is unusually high, at 37
feet (see http://modsimpy.com/wally).
We want to find the minimum velocity at which a ball can leave home
plate and still go over the Green Monster. We'll proceed in the
following steps:


For a given velocity, we'll find the optimal launch angle, that is, the angle the ball should leave home plate to maximize its height when it reaches the wall.


Then we'll find the minimal velocity that clears the wall, given
    that it has the optimal launch angle.


Finding the range
Suppose we want to find the launch angle that maximizes range, that is, the distance the ball travels in the air before landing. We'll use a function in the ModSim library, maximize_scalar, which takes a function and finds its maximum.
The function we pass to maximize_scalar should take launch angle in degrees, simulate the flight of a ball launched at that angle, and return the distance the ball travels along the $x$ axis.
End of explanation
"""


range_func(45, params)

"""
Explanation: range_func makes a new System object with the given value of
angle. Then it calls run_solve_ivp and
returns the final value of x from the results.
We can call range_func directly like this:
End of explanation
"""


angles = linspace(20, 80, 21)
sweep = SweepSeries()

for angle in angles:
    x_dist = range_func(angle, params)
    sweep[angle] = x_dist

"""
Explanation: And we can sweep a sequence of angles like this:
End of explanation
"""


sweep.plot()

decorate(xlabel='Launch angle (degree)',
         ylabel='Range (meter)')

"""
Explanation: Here's what the results look like.
End of explanation
"""


res = maximize_scalar(range_func, params, bounds=[0, 90])

"""
Explanation: It looks like the optimal angle is near 40°.
We can find the optimal angle more precisely and more efficiently using maximize_scalar, like this:
End of explanation
"""


res

res.x, res.fun

"""
Explanation: The first parameter is the function we want to maximize. The second is
the range of values we want to search; in this case, it's the range of
angles from 0° to 90°. 
The return value from maximize_scalar is an object that contains the
results, including x, which is the angle that yielded the highest
range, and fun, which is the value of range_func when it's evaluated at x, that is, range when the baseball is launched at the optimal angle.
End of explanation
"""


# Solution

def event_func(t, state, system):
    x, y, vx, vy = state
    return x - 94.5

# Solution

system = make_system(params)
event_func(0, system.init, system)

"""
Explanation: For these parameters, the optimal angle is about 41°, which yields a
range of 100 m.
Summary
If you enjoy this exercise, you might be interested in this paper: "How to hit home runs: Optimum baseball bat swing parameters for maximum range trajectories", by Sawicki, Hubbard, and Stronge, at
http://modsimpy.com/runs.
Exercise
Exercise: Let's finish off the Manny Ramirez problem:

What is the minimum effort required to hit a home run in Fenway Park?

Although the problem asks for a minimum, it is not an optimization problem.  Rather, we want to solve for the initial velocity that just barely gets the ball to the top of the wall, given that it is launched at the optimal angle.
And we have to be careful about what we mean by "optimal".  For this problem, we don't want the longest range, we want the maximum height at the point where it reaches the wall.
If you are ready to solve the problem on your own, go ahead.  Otherwise I will walk you through the process with an outline and some starter code.
As a first step, write an event_func that stops the simulation when the ball reaches the wall at 310 feet (94.5 m).
Test your function with the initial conditions.
End of explanation
"""


# Solution

def height_func(angle, params):
    params = params.set(angle=angle)
    system = make_system(params)

    results, details = run_solve_ivp(system, slope_func, 
                                     events=event_func)
    height = results.iloc[-1].y
    return height

# Solution

height_func(40, params)

"""
Explanation: Next, write a function called height_func that takes a launch angle, simulates the flight of a baseball, and returns the height of the baseball when it reaches the wall.
Test your function with the initial conditions.
End of explanation
"""


# Solution

res = maximize_scalar(height_func, params, bounds=[0, 90])
res

# Solution

res.x, res.fun

"""
Explanation: Now use maximize_scalar to find the optimal angle.  Is it higher or lower than the angle that maximizes range?
End of explanation
"""


# Solution

def error_func(velocity, params):
    print(velocity)
    params = params.set(velocity=velocity)
    res = maximize_scalar(height_func, params, bounds=[0, 90])
    return res.fun - 11.3

"""
Explanation: Even though we are finding the "minimum" velocity, we are not really solving a minimization problem.  Rather, we want to find the velocity that makes the height at the wall exactly 37 feet (11.3 m), given that it's launched at the optimal angle.  And that's a job for root_scalar.
Write an error function that takes a velocity and a System object as parameters.  It should use maximize_scalar to find the highest possible height of the ball at the wall, for the given velocity.  Then it should return the difference between that optimal height and 11.3 meters.
End of explanation
"""


# Solution

error_func(40, params)

"""
Explanation: Test your error function before you call root_scalar.
End of explanation
"""


# Solution

bracket = [30, 50]
res = root_scalar(error_func, params, bracket=bracket)

# Solution

res

# Solution

min_velocity = res.root
min_velocity

"""
Explanation: Then use root_scalar to find the answer to the problem, the minimum velocity that gets the ball out of the park.
End of explanation
"""


# Solution

error_func(min_velocity, params)

"""
Explanation: And just to check, run error_func with the value you found.
End of explanation
"""


