Skip to content

Commit a402c4f

Browse files
committedFeb 13, 2022
add sys argv lesson as alternate
1 parent 31269b0 commit a402c4f

File tree

3 files changed

+269
-42
lines changed

3 files changed

+269
-42
lines changed
 

‎_episodes/07-command_line.md

+2-1
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,8 @@ title: "Running code from the Linux Command Line"
33
teaching: 15
44
exercises: 10
55
questions:
6-
- "How do I move my code from the interactive jupyter notebook to run from the Linux command line?"
6+
- "How do I move my code from the interactive Jupyter notebook to run from the Linux command line?"
7+
- "How do I make Python scripts with help messages and user inputs using argparse?"
78
objectives:
89
- "Make code executable from the Linux command line."
910
- "Use argparse to accept user inputs."

‎_episodes/07-command_line_sys.md

+171
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,171 @@
1+
---
2+
title: "Running code from the Linux Command Line"
3+
teaching: 15
4+
exercises: 10
5+
alternate: true
6+
questions:
7+
- "How do I move my code from the interactive Jupyter notebook to run from the Linux command line?"
8+
- "How do I make simple Python scripts with user inputs using sys.argv?"
9+
objectives:
10+
- "Make code executable from the Linux command line."
11+
- "Use sys.argv() to accept user inputs."
12+
keypoints:
13+
- "You must `import sys` in your code to accept user arguments."
14+
- "The name of the script itself is always `sys.argv[0]` so the first user input is normally `sys.argv[1]`."
15+
---
16+
## Creating and running a python input file
17+
18+
We are now going to move our geometry analysis code out of the Jupyter notebook and into a format that can be run from the Linux command line. Open your favorite text editor and create a new file called "geom_analysis.py" (or choose another filename, just make sure the extension is .py). Paste in your geometry analysis code (the version with your functions) from your jupyter notebook and save your file.
19+
20+
The best practice is to put all your functions at the top of the file, right after your import statements. Your file will look something like this.
21+
```
22+
import numpy
23+
import os
24+
25+
def calculate_distance(atom1_coord, atom2_coord):
26+
x_distance = atom1_coord[0] - atom2_coord[0]
27+
y_distance = atom1_coord[1] - atom2_coord[1]
28+
z_distance = atom1_coord[2] - atom2_coord[2]
29+
bond_length_12 = numpy.sqrt(x_distance**2+y_distance**2+z_distance**2)
30+
return bond_length_12
31+
32+
def bond_check(atom_distance, minimum_length=0, maximum_length=1.5):
33+
if atom_distance > minimum_length and atom_distance <= maximum_length:
34+
return True
35+
else:
36+
return False
37+
38+
def open_xyz(filename):
39+
xyz_file = numpy.genfromtxt(fname=filename, skip_header=2, dtype='unicode')
40+
symbols = xyz_file[:,0]
41+
coord = (xyz_file[:,1:])
42+
coord = coord.astype(numpy.float)
43+
return symbols, coord
44+
45+
file_location = os.path.join('data', 'water.xyz')
46+
symbols, coord = open_xyz(file_location)
47+
num_atoms = len(symbols)
48+
for num1 in range(0,num_atoms):
49+
for num2 in range(0,num_atoms):
50+
if num1<num2:
51+
bond_length_12 = calculate_distance(coord[num1], coord[num2])
52+
if bond_check(bond_length_12) is True:
53+
print(F'{symbols[num1]} to {symbols[num2]} : {bond_length_12:.3f}')
54+
```
55+
{: .language-python}
56+
57+
Exit your text editor and go back to the command line. Now all you have to do to run your code is type
58+
59+
```
60+
$ python geom_analysis.py
61+
```
62+
{: .language-bash}
63+
in your Terminal window. Your code should either print the output to the screen or write it to a file, depending on what you have it set up to do. (The code example given prints to the screen.)
64+
65+
## Changing your code to accept user inputs
66+
In your current code, the name of the xyzfile to analyze, "water.xyz", is hardcoded; in order to change it, you have to open your code and change the name of the file that is read in. If you were going to use this code to analyze geometries in your research, you would probably want to be able to specify the name of the input file when you run the code, so that you don't have to change it every single time. These types of user inputs are called *arguments* and to make our code accept arguments, we have to import a new python library in our code.
67+
68+
Open your geometry analysis code in your text editor and add this line at the top.
69+
70+
~~~
71+
import sys
72+
~~~
73+
{: .language-python}
74+
75+
Now that you have imported the `sys` library, you can use its functions. The library has a function called `sys.argv()` which creates a list of all the arguments the user enters at the command line. Everything after *python* is an argument, so `sys.argv[0]` is always the name of your script. We would like our code to accept the name of the xyz file we want to analyze as an argument. Add this line to your code.
76+
```
77+
xyzfilename = sys.argv[1]
78+
```
79+
{: .language-python}
80+
81+
Then you need to go the part of your code where you read in the data from the xyz file and change the name of the file to read to `xyzfilename`.
82+
```
83+
symbols, coord = open_xyz(xyzfilename)
84+
```
85+
{: .language python}
86+
87+
Save your code and go back to the Terminal window. Make sure you are in the directory where your code is saved and type
88+
```
89+
$ python geom_analysis.py data/water.xyz
90+
```
91+
Check that the output of your code is what you expected.
92+
93+
94+
What would happen if the user forgot to specify the name of the xyz file? The way the code is written now, it would give an error message.
95+
```
96+
Traceback (most recent call last):
97+
File "geom_analysis.py", line 22, in <module>
98+
xyzfilename = sys.argv[1]
99+
IndexError: list index out of range
100+
```
101+
{: .error}
102+
The reason it says the list index is out of range is because `sys.argv[1]` does not exist. Since the user forgot to specify the name of the xyz file, the `sys.argv` list only has one element, `sys.argv[0]`. It would be better to print an error message and let the user know that they didn't enter the input correctly. Our code is expecting exactly two inputs: the script name and the xyz file name. The easiest way to add an error message is to check the length of the sys.argv list and print an error message and exit if it does not equal the expected length.
103+
104+
While you have practiced coding, you have probably seen many error messages. We can actually raise errors in our code and write error messages to our users.
105+
```
106+
if len(sys.argv) < 2:
107+
raise NameError("Incorrect input! Please specify a file to analyze.")
108+
```
109+
{: .language-python}
110+
111+
This will exit the code and print our error message if the user does not specify a filename.
112+
113+
There are different types of errors you can raise. For example, you may want to raise a `TypeError` if you have data that is not the right type. If you want to learn more about raising errors, [see the official documenation from Python](https://docs.python.org/3/tutorial/errors.html)
114+
115+
We need to add one more thing to our code. When you write a code that includes function definitions and a main script, you need to tell python which part is the main script. (This becomes very important later when we are talking about testing.) *After* your import statements and function definitions and *before* you check the length of the `sys.argv` list add this line to your code.
116+
```
117+
if __name__ == "__main__":
118+
```
119+
{: .language-python}
120+
121+
Since this is an `if` statement, you now need to indent each line of your main script below this if statement. Be very careful with your indentation! Don't use a mixture of tabs and spaces!
122+
123+
Save your code and run it again. It should work exactly as before. If you now get an error message, it is probably due to inconsistent indentation.
124+
125+
```
126+
import os
127+
import numpy
128+
import sys
129+
130+
def calculate_distance(atom1_coord, atom2_coord):
131+
x_distance = atom1_coord[0] - atom2_coord[0]
132+
y_distance = atom1_coord[1] - atom2_coord[1]
133+
z_distance = atom1_coord[2] - atom2_coord[2]
134+
bond_length_12 = numpy.sqrt(x_distance**2+y_distance**2+z_distance**2)
135+
return bond_length_12
136+
137+
def bond_check(atom_distance, minimum_length=0, maximum_length=1.5):
138+
if atom_distance > minimum_length and atom_distance <= maximum_length:
139+
return True
140+
else:
141+
return False
142+
143+
def open_xyz(filename):
144+
xyz_file = numpy.genfromtxt(fname=filename, skip_header=2, dtype='unicode')
145+
symbols = xyz_file[:,0]
146+
coord = (xyz_file[:,1:])
147+
coord = coord.astype(numpy.float)
148+
return symbols, coord
149+
150+
151+
if __name__ == "__main__":
152+
153+
if len(sys.argv) < 2:
154+
raise NameError("Incorrect input! Please specify a file to analyze.")
155+
156+
157+
xyz_file = sys.argv[1]
158+
symbols, coord = open_xyz(xyz_file)
159+
num_atoms = len(symbols)
160+
161+
for num1 in range(0,num_atoms):
162+
for num2 in range(0,num_atoms):
163+
if num1<num2:
164+
bond_length_12 = calculate_distance(coord[num1], coord[num2])
165+
if bond_check(bond_length_12) is True:
166+
print(F'{symbols[num1]} to {symbols[num2]} : {bond_length_12:.3f}')
167+
168+
```
169+
{: .language-python}
170+
171+
{% include links.md %}

‎_includes/syllabus.html

+96-41
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
1-
{% include base_path.html %}
2-
31
{% comment %}
42
Display syllabus in tabular form.
53
Days are displayed if at least one episode has 'start = true'.
@@ -19,49 +17,51 @@ <h2 id="schedule">Schedule</h2>
1917
<tr>
2018
{% if multiday %}<td class="col-md-1"></td>{% endif %}
2119
<td class="{% if multiday %}col-md-1{% else %}col-md-2{% endif %}"></td>
22-
<td class="col-md-3"><a href="{{ relative_root_path }}{% link setup.md %}">Setup</a></td>
20+
<td class="col-md-3"><a href="{{ page.root }}{% link setup.md %}">Setup</a></td>
2321
<td class="col-md-7">Download files required for the lesson</td>
2422
</tr>
2523
{% for episode in site.episodes %}
26-
{% if episode.start %} {% comment %} Starting a new day? {% endcomment %}
27-
{% assign day = day | plus: 1 %}
28-
{% if day > 1 %} {% comment %} If about to start day 2 or later, show finishing time for previous day {% endcomment %}
29-
{% assign hours = current | divided_by: 60 %}
30-
{% assign minutes = current | modulo: 60 %}
31-
<tr>
32-
{% if multiday %}<td class="col-md-1"></td>{% endif %}
33-
<td class="{% if multiday %}col-md-1{% else %}col-md-2{% endif %}">{% if hours < 10 %}0{% endif %}{{ hours }}:{% if minutes < 10 %}0{% endif %}{{ minutes }}</td>
34-
<td class="col-md-3">Finish</td>
35-
<td class="col-md-7"></td>
36-
</tr>
24+
{% if episode.legacy == None %}
25+
{% if episode.start %} {% comment %} Starting a new day? {% endcomment %}
26+
{% assign day = day | plus: 1 %}
27+
{% if day > 1 %} {% comment %} If about to start day 2 or later, show finishing time for previous day {% endcomment %}
28+
{% assign hours = current | divided_by: 60 %}
29+
{% assign minutes = current | modulo: 60 %}
30+
<tr>
31+
{% if multiday %}<td class="col-md-1"></td>{% endif %}
32+
<td class="{% if multiday %}col-md-1{% else %}col-md-2{% endif %}">{% if hours < 10 %}0{% endif %}{{ hours }}:{% if minutes < 10 %}0{% endif %}{{ minutes }}</td>
33+
<td class="col-md-3">Finish</td>
34+
<td class="col-md-7"></td>
35+
</tr>
36+
{% endif %}
37+
{% assign current = site.start_time %} {% comment %}Re-set start time of this episode to general daily start time {% endcomment %}
3738
{% endif %}
38-
{% assign current = site.start_time %} {% comment %}Re-set start time of this episode to general daily start time {% endcomment %}
39-
{% endif %}
40-
{% assign hours = current | divided_by: 60 %}
41-
{% assign minutes = current | modulo: 60 %}
42-
<tr>
43-
{% if multiday %}<td class="col-md-1">{% if episode.start %}Day {{ day }}{% endif %}</td>{% endif %}
44-
<td class="{% if multiday %}col-md-1{% else %}col-md-2{% endif %}">{% if hours < 10 %}0{% endif %}{{ hours }}:{% if minutes < 10 %}0{% endif %}{{ minutes }}</td>
45-
<td class="col-md-3">
46-
{% assign lesson_number = lesson_number | plus: 1 %}
47-
{{ lesson_number }}. <a href="{{ relative_root_path }}{{ episode.url }}">{{ episode.title }}</a>
48-
</td>
49-
<td class="col-md-7">
50-
{% if episode.break %}
51-
Break
52-
{% else %}
53-
{% if episode.questions %}
54-
{% for question in episode.questions %}
55-
{{question|markdownify|strip_html}}
56-
{% unless forloop.last %}
57-
<br/>
58-
{% endunless %}
59-
{% endfor %}
39+
{% assign hours = current | divided_by: 60 %}
40+
{% assign minutes = current | modulo: 60 %}
41+
<tr>
42+
{% if multiday %}<td class="col-md-1">{% if episode.start %}Day {{ day }}{% endif %}</td>{% endif %}
43+
<td class="{% if multiday %}col-md-1{% else %}col-md-2{% endif %}">{% if hours < 10 %}0{% endif %}{{ hours }}:{% if minutes < 10 %}0{% endif %}{{ minutes }}</td>
44+
<td class="col-md-3">
45+
{% assign lesson_number = lesson_number | plus: 1 %}
46+
{{ lesson_number }}. <a href="{{ page.root }}{{ episode.url }}">{{ episode.title }}</a>
47+
</td>
48+
<td class="col-md-7">
49+
{% if episode.break %}
50+
Break
51+
{% else %}
52+
{% if episode.questions %}
53+
{% for question in episode.questions %}
54+
{{question|markdownify|strip_html}}
55+
{% unless forloop.last %}
56+
<br/>
57+
{% endunless %}
58+
{% endfor %}
59+
{% endif %}
6060
{% endif %}
61-
{% endif %}
62-
</td>
63-
</tr>
64-
{% assign current = current | plus: episode.teaching | plus: episode.exercises | plus: episode.break %}
61+
</td>
62+
</tr>
63+
{% assign current = current | plus: episode.teaching | plus: episode.exercises | plus: episode.break %}
64+
{% endif %}
6565
{% endfor %}
6666
{% assign hours = current | divided_by: 60 %}
6767
{% assign minutes = current | modulo: 60 %}
@@ -77,4 +77,59 @@ <h2 id="schedule">Schedule</h2>
7777
The actual schedule may vary slightly depending on the topics and exercises chosen by the instructor.
7878
</p>
7979

80-
</div>
80+
<h2>Alternate Lessons</h2>
81+
82+
<p>
83+
Alternate lessons are shown below.
84+
</p>
85+
86+
<table class="table table-striped">
87+
{% for episode in site.episodes %}
88+
{% if episode.alternate %}
89+
{% if episode.start %} {% comment %} Starting a new day? {% endcomment %}
90+
{% assign day = day | plus: 1 %}
91+
{% if day > 1 %} {% comment %} If about to start day 2 or later, show finishing time for previous day {% endcomment %}
92+
{% assign hours = current | divided_by: 60 %}
93+
{% assign minutes = current | modulo: 60 %}
94+
<tr>
95+
{% if multiday %}<td class="col-md-1"></td>{% endif %}
96+
<td class="{% if multiday %}col-md-1{% else %}col-md-2{% endif %}">{% if hours < 10 %}0{% endif %}{{ hours }}:{% if minutes < 10 %}0{% endif %}{{ minutes }}</td>
97+
<td class="col-md-3">Finish</td>
98+
<td class="col-md-7"></td>
99+
</tr>
100+
{% endif %}
101+
{% assign current = site.start_time %} {% comment %}Re-set start time of this episode to general daily start time {% endcomment %}
102+
{% endif %}
103+
{% assign hours = current | divided_by: 60 %}
104+
{% assign minutes = current | modulo: 60 %}
105+
<tr>
106+
{% if multiday %}<td class="col-md-1">{% if episode.start %}Day {{ day }}{% endif %}</td>{% endif %}
107+
<td class="{% if multiday %}col-md-1{% else %}col-md-2{% endif %}"></td>
108+
<td class="col-md-3">
109+
{% assign lesson_number = lesson_number | plus: 1 %}
110+
<a href="{{ page.root }}{{ episode.url }}">{{ episode.title }}</a>
111+
</td>
112+
<td class="col-md-7">
113+
{% if episode.break %}
114+
Break
115+
{% else %}
116+
{% if episode.questions %}
117+
{% for question in episode.questions %}
118+
{{question|markdownify|strip_html}}
119+
{% unless forloop.last %}
120+
<br/>
121+
{% endunless %}
122+
{% endfor %}
123+
{% endif %}
124+
{% endif %}
125+
</td>
126+
</tr>
127+
{% assign current = current | plus: episode.teaching | plus: episode.exercises | plus: episode.break %}
128+
{% endif %}
129+
{% endfor %}
130+
{% assign hours = current | divided_by: 60 %}
131+
{% assign minutes = current | modulo: 60 %}
132+
</table>
133+
134+
135+
</div>

0 commit comments

Comments
 (0)
Please sign in to comment.