INFO1110 Introduction to Programming - Assignment: Photonic circuit
Assignment (INFO1110)
Summary of Features
This assignment will involve implementing each feature one-by-one. Each feature involves implementing a set number of functions and classes which will be used in our program. Each feature is separate, meaning adding a new feature does not require any changes to your existing features to incorporate. In addition, when running the program, the user can specify which features they want ran in their program, meaning not necessarily every feature needs to be used. As example, we could create a circuit from inputs with mirrors added into it (Feature 1, 2 and 4), but not want to run it (Feature 3). This creates a very flexible and modular program that is easy to extend.
1. SET-MY-CIRCUIT
SET-MY-CIRCUIT will focus on implementing the necessities to setup a circuit. A circuit comprises of a board in which we can place components on it. Components include emitters (lasers that emits photons) labelled from A to J and receivers (photodetectors that absorb photons) labelled from
R0 to R9 . We do not yet implement the circuitry functionalities.
By the end of this feature, you will be able to setup a circuit and display it to the user. An example is we could setup a circuit with some fixed values; a board of size 18x6 characters, with two emitters A and B and two receivers R0 and R1 (receivers are displayed by their number). We should then be able to display it such as below.
+------------------+ || |B| |A 0| || |1| || +------------------+
2. GET-MY-INPUTS
GET-MY-INPUTS allows the user to specify what circuit they want to setup by providing inputs to the
program. From this, the user can setup a circuit to their own specifications. Below is an example of how a user will setup their circuit.
We prepended the inputs with a # symbol so you can clearly see what the inputs to the program are.
$ python3 run.py
Creating circuit board...
> #18 6
18x6 board created.
Adding emitter(s)...
> #A 2 2
> #B 8 1
> #END EMITTERS
2 emitter(s) added.
Adding receiver(s)...
> #R0 15 2
> #R1 8 4
> #END RECEIVERS
2 receiver(s) added.
+------------------+ || |B| |A 0| || |1| || +------------------+
It will first ask to get the size of the board. The size is given in the format <width> <height> . The example above creates a circuit board of size 18x6 .
It will then ask to add emitters on the board. The emitters are given in the format <symbol> <x> <y> . Users can add up to 10 emitters, where each emitter is uniquely labelled from A to J . The example above adds emitter A at position (2, 2) , followed by emitter B at position (8, 1) . Once users are done adding emitters, they enter END EMITTERS to move on with the program.
Similarly, it will then ask to add receivers on the board. Users can add up to 10 receivers, where each receiver is uniquely labelled from R0 to R9 . The example above adds receiver R0 at position (15, 2) then R1 at position (8, 4) . The user will be asked to keep adding receivers until 10 are added. Once users are done adding receivers, they enter END RECEIVERS to move on with the program. At this point, the only part left to do is to display the board.
By the end of this feature, you will have a run program that will be able to setup a circuit based on the user's inputs and display the board.
3. RUN-MY-CIRCUIT
RUN-MY-CIRCUIT is responsible for running the circuit. The first step is that it reads from a file the frequency and direction of each photon emitted. Each line of the file is in the format of <symbol> <frequency> <direction> where <symbol> is the symbol of the emitter which will emit the photon,
<frequency> is the frequency of the photon and <direction> is the direction in which the photon will travel.
This is called the pulse sequence. An example of a pulse_sequence.in file is shown below which is located in /home/input/ .
A 100 E B 256 S
The pulse sequence above defines that A will emit a photon at 100THz (terahertz) east and B will emit a photon at 256THz south. After loading the pulse sequence into the circuit, the circuit performs the following steps:
-
Eachemitteremitsaphoton.
-
Ineachtick(atickisananosecond),eachphotonmoves.Ifaphotonreachesareceiver,the receiver will absorb the photon.
-
Aftereachtick,weincrementourclock(asimplecounter)whichkeepstrackofhowlongour circuit has run for.
-
RepeatSteps2-3untilallphotonshavebeenabsorbed.
Once a receiver absorbs a photon, the receiver becomes activated in which it's charged with the
photon's energy. An activated receiver can continue absorbing more photons. Let's look at a small example of running a circuit.
+------------------+ || |B| |A 0| || |1| || +------------------+
With the given circuit, we load the pulse sequence defined above into it. Then, we provide some steps below to show what happens from here.
At 0ns (nanoseconds), the circuit emits photons. A emits a 100THz photon which will move to the east and B emits a 256THz photon which will move to the south. When emitting the photons, the photons initially start inside the emitter (hence the diagram looks no different).
+------------------+ || |B| |A 0| || |1| ||
+------------------+
At 1ns, each photon moves one unit on the board at their given directions.
+------------------+ || |B| |A.. 0| || |1| || +------------------+
Similarly for 2ns, each photon moves one unit on the board. You can see that we draw the path each photon takes. You can picture it as the board being made of sand, and each time the photon moves, it leaves a footstep on the ground.
+------------------+ || |B| |A... 0| |.| |1| || +------------------+
At 3ns, the photon emitted from B has reached R1 , hence has been absorbed by this receiver. R1 is now activated.
+------------------+ || |B| | A... . 0 | |.| |1| || +------------------+
At 13ns, R0 is activated as it absorbs the photon emitted from A .
+------------------+ || |B| | A............0 | |.| |1| || +------------------+
Running the circuit is now completed as all photons have been absorbed.
By the end of this feature, you will have a run program that can optionally run a circuit given that a -RUN-MY-CIRCUIT flag is included as an argument. If it is not there, it will not run the circuit. Below is
an example run through of the program.
$ python3 run.py -RUN-MY-CIRCUIT
Creating circuit board...
> #18 6
18x6 board created.
Adding emitter(s)...
> #A 2 2
> #B 8 1
> #END EMITTERS
2 emitter(s) added.
Adding receiver(s)...
> #R0 15 2
> #R1 8 4
> #END RECEIVERS
2 receiver(s) added.
+------------------+ || |B| |A 0| || |1| || +------------------+
<RUN-MY-CIRCUIT FLAG DETECTED!>
Setting pulse sequence...
-- (A, B)
Line 1: #A 100 E
-- (B)
Line 2: #B 256 S Pulse sequence set.
======================== RUNNING CIRCUIT... ========================
0ns: Emitting photons.
A: 100THz, East
B: 256THz, South
5ns: 1/2 receiver(s) activated. +------------------+ ||
We prepended the inputs with a # symbol so you can clearly see what the inputs to the program are.
|B| | A...... 0 | |.| |1| || +------------------+
10ns: 1/2 receiver(s) activated. +------------------+ || |B|
| A.......... 0 | |.| |1| || +------------------+
13ns: 2/2 receiver(s) activated. +------------------+ || |B|
| A............0 | |.| |1| || +------------------+
Activation times:
R1: 3ns
R0: 13ns
Total energy absorbed:
R1: 1.06eV (1)
R0: 0.41eV (1)
======================== CIRCUIT FINISHED!
========================
Here are a few notes about the output above:
When setting the pulse sequence, you may have noticed the lines -- (A, B) and -- (B) . These are showing the remaining emitters to set the pulse sequence for after each input read from the pulse_sequence.in file.
When running the circuit, the state of the board is printed every 5ns, along with how many receivers have been activated. We print the circuit one last time when all photons have been absorbed, which in this case is at 13ns.
The activation times are printed in ascending order. You can see R1 is first which was activated at time 3ns. Next was R0 which was activated at 13ns.
Next, the total energy absorbed is printed in descending order. The energy absorbed is
displayed in electronvolts (eV) and the number following it is the number of photons absorbed by the receiver. In this case, R1 stores 1.06eV and absorbed 1 photon. Similarly, R2 stores 0.41eV and absorbed 1 photon
You will find out how to convert THz to electronvolts (eV) in RUN-MY-CIRCUIT . For now, just know that a higher frequency (THz) means a higher energy (eV).
4. ADD-MY-MIRRORS
ADD-MY-MIRRORS allows the user to add mirrors into the circuit. Mirrors are able to reflect photons off its surface which changes the direction in which the photons travel. Mirrors are a component just like emitters and receivers.
Mirrors are given in the format <symbol> <x> <y> , similar to emitters and receivers. There are 4 different types of mirrors which include / , \ , > and ^ . We won't go in the details for each mirror, just know that it will reflect photons off it depending on both the type of mirror and the direction of the photon. Below is an example valid input of a mirror.
\25
This would create a mirror / and place it at position (2, 5) on the circuit board. Since mirrors aren't uniquely labelled (you can have multiple mirrors with the same symbol), users can enter as many mirrors as they want, as long as there is space for it. Once they are finished, they can stop adding mirrors by entering END MIRRORS .
By the end of this feature, you will have a run program that can optionally include mirrors given that the -ADD-MY-MIRRORS flag is included as an argument. If it is not there, it will not involve mirrors in the program. Below is an example run through of the program.
$ python3 run.py -RUN-MY-CIRCUIT -ADD-MY-MIRRORS
Creating circuit board...
> #18 6
18x6 board created.
Adding emitter(s)...
> #A 2 2
> #B 8 1
> #END EMITTERS
2 emitter(s) added.
Adding receiver(s)...
> #R0 15 2
> #R1 8 4
> #END RECEIVERS
2 receiver(s) added.
We prepended the inputs with a # symbol so you can clearly see what the inputs to the program are.
<ADD-MY-MIRRORS FLAG DETECTED!>
Adding mirror(s)...
> #\ 2 5
> #^ 5 5
> #/ 5 4
> #\ 11 1
> #> 11 4
> #/ 15 4
> #END MIRRORS
6 mirror(s) added.
+------------------+ || |B\| |A 0| || |/1>/| |\^ | +------------------+
<RUN-MY-CIRCUIT FLAG DETECTED!
Total energy absorbed:
R0: 1.06eV (1)
R1: 0.41eV (1)
======================== CIRCUIT FINISHED!
========================
SET-MY-CIRCUIT (5 marks) Introduction A circuit board is defined on a two dimensional plane. +------------------+ || || || || || || +------------------+ Each circuit can have up to 10 emitters and 10 receivers. Photons are emitted from emitters A to J which may be received by a receiver R0 to R9 . The receivers on the board are displayed by their number. +------------------+ || |B| |A 0| || |1| || +------------------+ We can see that the circuit board has size 18x6 . The size does not include the border around the board. These are the positions of the emitters and receivers. The top left corner has position (0, 0) . A: (2, 2) B: (8, 1) R0: (15, 2) R1: (8, 4) This should be enough information to cover the context of this feature, but you can scout for additional information in Assignment Breakdown under Section 1. SET-MY-CIRCUIT if needed. Your Task |
The SET-MY-CIRCUIT feature will allow you to setup a circuit board just like the example in the above section. In an implementation perspective, you'll be able to make a LaserCircuit instance and add your own Emitter and Receiver instances into it. You will also be able to output the board on the screen.
In this feature, you will be adding implementation to the following:
Emitter class Receiver class BoardDisplayer class LaserCircuit class
1. Emitter
An Emitter instance represents a laser which emits a photon with a frequency (THz) and direction (either right or downwards).
These are the instance attributes of an Emitter instance.
When initialising an Emitter instance, it is given a symbol from A to J which is how this emitter will be shown on the board. The emitter is given an x and y position which is its position on the board. At the start, frequency is 0 and direction is None . This is later set by the pulse sequence once we get up to running the circuit, hence the reason why pulse_sequence_set also starts false.
There may be some functionalities in these classes that will be skipped. It will be specified what you need to implement for this feature.
component_type is a constant with value 'emitter' which acts as an identity string for all emitters. These are the instance methods of an Emitter instance that you'll need to implement for this
feature.
2. Receiver
A Receiver instance represents a photodetector that charges up by absorbing photons. When a receiver absorbs a photon, it becomes activated in which it is charged with the photon's energy. An activated receiver can keep absorbing more photons to store more energy. A receiver stores information about the number of photons it has absorbed, the total energy it is charged with and when it was activated. The amount of energy a receiver is charged with directly corresponds to the frequency of all photons it absorbs summed up.
These are the instance attributes of a Receiver instance.
When initialising a Receiver instance, it is given a symbol from R0 to R9 . The number ( 0 to 9 ) is what will be used to show the receiver on the board. The receiver is given an x and y position which is its position on the board. At the start, total_energy is 0.0, photons_absorbed is 0 as it starts with no photons absorbed. Similarly, activated is false and activation_time is 0 as it must absorb a photon to become activated. component_type is a constant with value 'receiver' which acts as an identity string for all receivers.
These are the instance methods of a Receiver instance that you'll need to implement for this feature.
3. BoardDisplayer
A BoardDisplayer instance is responsible for displaying the circuit board. It's considered a helper class as it does not assist with the functionality of the circuit. Its only purpose is to store a list of list of strings (2 dimensional list of strings) representing the board. Each time a component is added to the circuit, the BoardDisplayer instance is updated to store the component's symbol in its assigned position in the 2D list.
These are the instance attributes of a BoardDisplayer instance.
When initialising a BoardDisplayer instance, it is given a width and height which will then be
used to initialise an empty board of that size.
These are the instance methods of a BoardDisplayer instance that you'll need to implement for this
feature.
Below we will provide clarifications for some of the instance methods to implement.
3.1 create_board
The create_board method takes in a width and height and will create a list of list of strings representing an empty board of size width x height . As example, let's say width is 6 and height is 3. Then the function needs to return the following:
[
[' ', ' ', ' ', ' ', ' ', ' '],
[' ', ' ', ' ', ' ', ' ', ' '],
[' ', ' ', ' ', ' ', ' ', ' ']
]
You can see it's one list containing 3 inner-lists, representing our height. These are the rows.
Each row contains 6 elements, representing the width. These are the columns. Each element is a single space, representing an empty cell on the board.
3.2 add_component_to_board
The add_component_to_board method takes in a component and adds its symbol on the board at its assigned position.
In the BoardDisplayer constructor, the board instance attribute should be given the return value of the create_board method.
Let's take the empty board we just made, and say we call the add_component_to_board method twice. In the first call, we passed an emitter with symbol A and position (3, 1) and in the second call, we passed a receiver with symbol R9 and position (0, 2) . Then, the board attribute will now look like this.
[
[' ', ' ', ' ', ' ', ' ', ' '],
[' ', ' ', ' ', 'A', ' ', ' '],
['9', ' ', ' ', ' ', ' ', ' ']
]
A is at board[1][3] and R9 is at board[2][0] .
3.3 print_board
For print_board , it will translate the board attribute into output. Let's take the example above
where we just put in an emitter and a receiver. After calling the method, it would print the following:
+------+ || |A| |9 | +------+
You can see the border is not included in the size (it wraps around the board).
4. Laser Circuit
A LaserCircuit instance is responsible for storing all components of the circuit and handling the computation of running the circuit. It's responsible for delegating tasks to the specific components e.g. making each emitter emit a photon, getting each photon to move and interact with components, etc. In general, this class is responsible for handling any task related to the circuit.
These are the instance attributes of a LaserCircuit instance.
It shouldn't scare you that it can accept different types of components. They all have common properties, that being their symbol , x and y value. Since every component has these attributes, it doesn't actually matter what component is passed in, you can treat them all the same.
To access position (x, y) on the board, it is board[y][x] as the first index represents the row (height) and the second index represents the column (width).
When initialising a LaserCircuit instance, it is given a width and height used to create the board_displayer . The laser circuit initially starts with 0 components, so the list of emitters , receivers and mirrors start empty. Similarly, the list of photons start empty as no photons have
been emitted. clock starts at 0, this will increment once we begin running the circuit.
These are the instance methods of a LaserCircuit instance that you'll need to implement for this feature.
4.1 print_board
The print_board method is straightforward. We simply call the print_board method of
board_displayer . This is so when we have a LaserCircuit instance, we can easily print the board. 4.2 get_collided_emitter
The get_collided_emitter checks if entity has the same x and y value as any emitter in the circuit and returns that emitter if there exists a collision, else it returns None . Remember that all components and photons have an x and y value, so we shouldn't need to worry about what specific entity was passed in to check the collision. This also applies for get_collided_receiver in the context that we are checking receivers instead of emitters.
Below are more methods that need to be implemented for this feature.
4.3 add_emitter
For add_emitter , here are the error messages for the checks you must perform.
Note from the method description that if at any point an error occurs, you return False . As example, if the emitter is out-of-bounds, after printing the error message you return False , meaning the remaining 2 checks are skipped.
Below are more methods that need to be implemented for this feature.
4.4 add_receiver
Similarly, here are the error messages for the checks you must perform. They mostly borrow from add_emitters .
Below are the remaining methods that need to be implemented for this feature.
Let's run through of an example of initialising a LaserCircuit instance, printing the board, then adding some components and printing the board again. We show an example of each error message that can occur when
>>> e1 = Emitter('A', 2, 2)
>>> e2 = Emitter('B', 8, 1)
# make an extra emitter to show error
>>> e3 = Emitter('C', 8, 1)
# add the emitters and print the board
>>> circuit.add_emitter(e1)
>>> circuit.add_emitter(e2)
>>> circuit.add_emitter(e3)
Error: position (8, 1) is already taken by emitter 'B'
>>> circuit.print_board()
+------------------+ || |B| |A| || || || +------------------+
# create out receivers
>>> r1 = Receiver('R0', 15, 2)
>>> r2 = Receiver('R1', 8, 4)
# make extra receivers to show error
>>> r3 = Receiver('R1', 9, 0)
>>> r4 = Receiver('R1', 0, 13)
# add the receivers and print the board
>>> circuit.add_receiver(r1)
>>> circuit.add_receiver(r2)
>>> circuit.add_receiver(r3)
Error: symbol 'R1' is already taken
>>> circuit.add_receiver(r4)
Error: position (0, 13) out-of-bounds on 18x6 circuit board
>>> circuit.print_board()
+------------------+
||
|B| |A 0| || |1| || +------------------+
GET-MY-INPUTS (5 marks)
Introduction
In SET-MY-CIRCUIT , we implemented some classes to provide the base for building our circuit. Now we want to allow the user to provide inputs to the program in which they will be able to create a circuit to their own specifications.
On top of what's covered on Assignment Breakdown under Section 2. GET-MY-INPUTS , we will also be covering input parsing. By this, we mean when users enter any input, whether it be for the board size, emitters, or receivers, you will need to parse the input to validate that the input is correct.
An example of input parsing is shown below, where the user enters an incorrect size for the circuit board multiple times.
Creating circuit board...
> #18
Error: <width> <height>
> #18 six
Error: height must be an integer
> #18 6
18x6 board created.
Until the user enters a valid size, the program will keep asking for input. You can see entering only the width displayed an error message Error: <width> <height> . The program then asked for input again, in which the user incorrectly entered the height, displaying an error message Error: height must be an integer.Onthethirdinput,theuserenteredavalidsize,creatingtheboardwherethe program can proceed.
Your Task
The GET-MY-INPUTS feature will allow the user to set up a circuit to their specifications using inputs. From an implementation perspective, you'll be making a LaserCircuit instance and adding
Emitter and Receiver instances into it based on the user's inputs. Then, you'll display the circuit board to the user.
Inputs are prepended with a # in the output snippets for your own clarity.
This is just a small look at the input parsing, there will be more to cover.
In this feature, you will be adding implementation to the following: input_parser module
run module
1. Input Parser
The input_parser module is responsible for parsing the inputs of the program. We define parsing as checking the validity of what has been entered to determine if it's valid. If it's not valid, an appropriate error message should be printed to indicate what was wrong with the input. Whenever we retrieve input in the program, we should be using functions from this module to validate it.
These are the functions in the input_parser module that you'll need to implement for this feature.
1.1 parse_size
For parse_size , here are the error messages for the checks you must perform.
There may be some functionalities in these modules that will be skipped. It will be specified what you need to implement for this feature.
Here are some examples of running parse_size in a Python interactive session.
>>> from input_parser import parse_size
# Error 1
>>> size = parse_size('6')
Error: <width> <height>
>>> size
None
# Error 2
>>> size = parse_size('six 3')
Error: width is not an integer
>>> size
None
# Error 3
# Note: width is not positive (Error 4), however Error 3 is checked first
>>> size = parse_size('-6 three')
Error: height is not an integer
>>> size
None
# Error: 4
# Note: height is not positive (Error 5), however Error 4 is checked first
>>> size = parse_size('-6 -3')
Error: width must be greater than 0
>>> size
None
# Error 5
>>> size = parse_size('6 0')
Error: height must be greater than 0
Note from the method description that if at any point an error occurs, you return None . As example, if the first check passes but the second check fails (where width is not an integer), we return None , meaning the remaining 3 checks are skipped.
Output Snippet 1 - Calling parse_size in a Python interactive session.
>>> size None
# No errors
>>> size = parse_size('6 3')
>>> size
(6, 3)
Below are more methods that need to be implemented for this feature.
1.2 parse_emitter
For parse_emitter , here are the error messages for the checks you must perform.
Here are some examples of running parse_emitter in a Python interactive session.
>>> from input_parser import parse_emitter
# Error 1
>>> emitter = parse_emitter('A 0')
Error: <symbol> <x> <y>
>>> emitter
None
# Error 2
>>> emitter = parse_emitter('K 0 0')
Error: symbol is not between 'A'-'J'
>>> emitter
None
# Error 3
# Note: y is not an integer (Error 4), however Error 3 is checked first
>>> emitter Error: x is >>> emitter None
# Error 4
>>> emitter Error: y is >>> emitter None
= parse_emitter('A B C') not an integer
= parse_emitter('A 0 zero') not an integer
# Error 5
The checks and error messages are identical for parse_receiver , with the only exception being when checking the symbol. Instead of A to J , we need to check from R0 to R9 ,. and the error message would be
Error: symbol is not between R0-R9.
Output Snippet 2 - Calling parse_emitters in a Python interactive session.
>>> emitter = parse_emitter('A -1 0')
Error: x cannot be negative
>>> emitter
None
# Error 6
>>> emitter = parse_emitter('A 0 -3')
Error: y cannot be negative
>>> emitter
None
# No errors
>>> emitter = parse_emitter('A 0 0')
>>> emitter
<Object: Emitter> # an emitter instance, just shown as this for readability
2. run
The run module is responsible for running the entire program. In summary, it needs to take in the inputs, process them into creating the circuit, and then print the board. A large part of the implementation will revolve around calling functions and creating instances of classes we have implemented from previous works.
These are the functions of the run module that you'll need to implement for this feature.
2.1 initialise_circuit
This function is responsible for getting the inputs of the board size, emitters, and receivers and
creating the circuit. These are the general steps for this function.
1. Getsinputfromtheuserfortheboardsizetocreateacircuit.
2. Until the maximum amount of emitters are added (10) or END EMITTERS is inputted by the user, for each input:
1. Createanewemitterwiththespecifiedvalues. 2. Addthenewemitterintothecircuit.
3. Until the maximum amount of receivers is added (10), or END RECEIVERS is inputted by the user, for each input:
1. Create a new Receiver instance with the specified values. 2. Addthenewemitterintothecircuit.
The steps above don't include the instance when an invalid input is given. If an invalid input is given, it will simply print an error message stating the cause of the error and ask for input again.
1. Size
This is an example of entering the board size to create the circuit board.
Creating circuit board...
> #18 6
18x6 board created.
Now let's look at an example with some errors:
Creating circuit board...
> #18 0
Error: height must be greater than zero
> #18
> #D 2 1
> #E 3 3
> #C 4 1
> #H 5 5
> #G 6 1
> #F 7 4
> #I 8 1
> #J 9 6
10 emitter(s) added.
Now let's look at an example with some errors:
Adding emitter(s)...
> #A 2 2
> #A 3 4
Error: symbol 'A' is already taken
> #B 2 2
Error: position (2, 2) is already taken by emitter 'A'
> #B 8 1
> #END EMITTERS
2 emitter(s) added.
You can see that the program will continuously take in input until either 10 emitters are added, or they end it explicitly with END EMITTERS . Notice the above is what we have just implemented in
parse_emitter and add_emitter , how convenient!
3. Receivers
This is an example of entering the values to add new emitters to the circuit board.
Adding receiver(s)...
> #R0 15 2
> #R1 8 4
> #END RECEIVERS
2 receiver(s) added.
You can see it's essentially the same input format as adding emitters.
Adding receiver(s)...
> #R0 15 2
> #R0 8 4
Error: symbol 'R0' is already taken
> #R1 15 2
Error: position (15, 2) is already taken by receiver 'R0'
> #R1 8 4
> #END RECEIVERS
2 receiver(s) added.
4. Putting it Together
You can see that the emitters and receivers are sorted, regardless of what order it is added in from
the inputs.
2.2 main
The main function takes in one argument args which is the command line arguments of the program. For now, you can ignore this. This function for now will simply just call
initialise_circuit , get the LaserCircuit instance and use its methods to print the board. It should be the shortest function in this module!
>>> from run import main
>>> main([])
Creating circuit board...
> #18 6
18x6 board created.
Adding emitter(s)...
> #A 2 2
> #B 8 1
> #END EMITTERS
2 emitter(s) added.
Adding receiver(s)...
> #R0 15 2
> #R1 8 4
> #END RECEIVERS
2 receiver(s) added.
+------------------+ || |B| |A 0| || |1| || +------------------+
Output Snippet 6 - Output from calling main function
Inputs are prepended with a # in the output snippets for your own clarity.
+------------+ |BG J| | AECH| |DF I| |012 34 56789| +------------+
Output Snippet 7 - Output from run program
Inputs are prepended with a # in the output snippets for your own clarity.
RUN-MY-CIRCUIT (7 marks) Introduction In GET-MY-INPUTS , we have allowed users to set up a circuit based on the their inputs. Now we want to be able to run the circuit, which comprise of photons travelling across the circuit board and interacting with components. Please have a read of the Assignment Breakdown under Section 3. RUN-MY-CIRCUIT as it covers the basis for this entire feature. There are only some very small parts on top of what's covered on that page that need to be implemented which will be explained once we get to it. This feature requires the most implementation out of the rest, so we have broke it down into two parts for simplicity. Part 1 (15%) focuses on implementing individual functionalities such as moving photons, displaying photon paths, energy conversions, etc. Part 2 (20%) will then cover putting everything together so we can run the circuit from start-to-end when running the program. Your Task The RUN-MY-CIRCUIT feature will allow the user to run their circuit. They can optionally choose to do so by adding the -RUN-MY-CIRCUIT flag as an argument. You can view it as an extension of the existing program. In Part 1 (Sections 1-5), you will be adding implementation to the following: Photon class This will cover implementing the individual functionalities for running the circuit. In Part 2 (Sections 6-8), you will be adding implementation to the following: LaserCircuit class input_parser module run module |
This will cover putting everything together so we can run the circuit from start-to-end when running the program.
Part 1 will cover implementing the logic for the photons and components.
1. Photon
A Photon instance is a particle of light that is emitted by an Emitter instance and travels along the circuit board. Photons have a frequency (THz) and direction in which they move in. They can interact with components in the circuit such as a Receiver instance in which it can be absorbed.
These are the instance attributes of a Photon instance.
When initialising a Photon instance, it takes in a x and y position, as well as a frequency and direction . These values will come from the emitter it is emitted from. Initially, absorbed is false. All
photons have a dot as their symbol.
These are the instance methods of a Photon instance that you'll need to implement for this feature.
1.1 move
When a photon moves, it moves one unit depending on its direction.
direction : 'N' , y is decremented (moving up). direction : 'E' , x is incremented (moving right). direction : 'S' , y is incremented (moving down). direction : 'W' , x is decremented (moving left).
Here are some examples of running move in a Python interactive session.
>>> from photon import Photon
>>> photon = Photon(0, 0, 100, 'S')
# initial position
>>> photon.x, photon.y (0, 0)
Output Snippet 1 - Calling instance method move in Python interactive session.
# move up
# 2, 2 is the board with and height respectively
>>> photon.move(2, 2)
>>> photon.x, photon.y
(0, 1)
# move right
>>> photon.set_direction('E')
>>> photon.move(2, 2)
>>> photon.x, photon.y
(1, 1)
# going out of bounds
>>> photon.move(2, 2)
>>> photon.x, photon.y
(1, 1)
# notice the position is still (1, 1), we do not allow it to move out of the circuit
# now the photon is absorbed
>>> photon.absorbed True
1.2 interact_with_component
Below are the remaining methods that need to be implemented for this feature.
You may find it easier to implement this method once you have at least read the implementation for Receiver in section 3.
2. Emitter
You will be finishing the implementation of the Emitter instance. These methods relate to the running of the circuit.
Below we will provide clarifications for some of the instance methods to implement.
2.1 emit_photon
For further clarification on emit_photon() , this method needs to create and return a Photon
instance. This photon should inherit the position, frequency, and direction of the emitter.
>>> from emitter import Emitter >>> emitter = Emitter('A', 0, 0)
>>> emitter.set_pulse_sequence(100, 'N') # get the photon emitted from the emitter
Output Snippet 2 - Calling instance method emit_photon of Emitter instance in Python interactive session.
-
>>> photon = emitter.emit_photon()
-
>>> photon.x, photon.y
(0, 0)
-
>>> photon.frequency
100
-
>>> photon.direction 'N'
2.2 __str__
For the __str__ method, printing the emitter instance itself should output the custom format string. As an example, if we had an emitter A that has frequency set to 100 and the direction set to 'N' , this would be the output.
-
>>> from emitter import Emitter
-
>>> emitter = Emitter('A', 0, 0)
-
>>> emitter.set_pulse_sequence(100, 'N')
-
>>> emitter.frequency 100
-
>>> emitter.direction 'N'
-
>>> print(emitter) A: 100THz, North
3. Receiver
You will be finishing the implementation of the Receiver instance. These functions and methods relate to the running of the circuit.
Output Snippet 3 - Printing an Emitter instance in Python interactive session.
Below we will provide clarifications for some of the instance methods to implement.
3.1 convert_frequency_to_energy
To calculate how much energy a photon carries, we use a constant that relates a photon's frequency
to its energy in joules known as the Planck Constant h.
h = 6.62607015 × 10−34J ⋅ Hz−1
The Planck Constant is a very significant physical constant in quantum mechanics as it describes the behaviour of particles on the atomic scale. In summary, it defines the quantum nature of energy and relates the energy of a photon to its frequency. It is this theory that electromagnetic energy cannot flow continuously, and must transfer energy through discrete values, allowing us to mathematically compute how the universe operates on an atomic scale such as stellar evolution (the lifetime of a
We promised we would not involve any physics in this assignment, so we have implemented this function for you. However, it cannot hurt to get some context behind its workings. 😄
The unit J ⋅ Hz−1 represents joules per hertz. Frequency (Hz) is defined as a cycle per second, hence Hz−1 can be substituted as s−1 or more intuitively 1/s.
star).
Let's say that we have a photon with 256THz. To calculate its energy we have the given equation
E = h × f , where given Planck's constant h and the frequency of the photon f in hertz (Hz), we can
calculate its energy E in joules (J). Since 1THz is equal to 1012 Hz, we simply just need to multiply it
by a factor of 12 to get our units correct, giving us the following equation.
E = h × 256 × 1012
The final part is that E is in joules. We want it to be in electronvolts (eV). By definition, one electronvolt is the energy acquired by an electron when it is accelerated through a potential difference of 1 volt. We have it that 1eV is equal to 1.60217662 × 10−19J. So we simply need to divide E by this amount.
eV= E 1.60217662 ×10−19J
Hence, let us say we want to convert 256THz to electronvolts. The equation is given as follows.
6.62607015 × 10−34 × 256 × 1012 J ⋅ Hz−1
eV = 1.60217662 × 10−19J = 1.06 (2 dp)
Knowing any of the above is likely not helpful for the assignment, but you will need to use this function to convert frequencies to energy.
3.2 absorb_photon
The absorb_photon is responsible for handling the logic for the receiver absorbing a photon. You will need to mathematically convert the photon's frequency (THz) to energy (eV) so we can correctly add to our total energy.
>>> from receiver import Receiver >>> from photon import Photon
# create our receiver
>>> receiver = Receiver('R0', 0, 0)
# absorb first photon
>>> p1 = Photon(0, 0, 256, 'R')
>>> receiver.absorb_photon(p1, 15)
>>> receiver.total_energy
1.058730939663818
>>> receiver.photons_absorbed
1
>>> receiver.activation_time
Output Snippet 4 - Calling instance method absorb_photon of Receiver instance in Python interactive session
15
# absorb second photon
>>> p2 = Photon(0, 0, 300, 'D')
>>> receiver.absorb_photon(p2, 25)
# it should be adding to the existing total energy
>>> receiver.total_energy
2.299431259582355
>>> receiver.photons_absorbed
2
# notice that the activation time does not change once it has been set
>>> receiver.activation_time
15
3.3 __str__
Printing the receiver instance itself should result in the formatted string returned by __str__ being printed. The example below shows a receiver R0 that has absorbed 2 photons, the first having a frequency of 69THz absorbed at 60ns and the second being 420THz at 120ns.
>>> from receiver import Receiver >>> from photon import Photon
>>> receiver = Receiver('R0', 0, 0)
>>> p1 = Photon(0, 0, 69, 'R')
>>> p2 = Photon(0, 0, 420, 'R')
>>> receiver.absorb_photon(p1, 60) >>> receiver.absorb_photon(p2, 120)
>>> print(receiver) R0: 2.02eV (2)
4. BoardDisplayer
You will be finishing the implementation of the instance methods in the BoardDisplayer class.
This method is very similar to the add_component_to_board method but it is dedicated for Photon instances. The only key difference in terms of placing the symbol on the board is that it should only be placed if the cell is empty. The logic is that components have a higher priority being shown in
comparison to a photon, and replacing it with another photon makes no difference. Let's take this simple example of a board .
[
[' ', ' ', ' ', ' ', ' '],
['A', ' ', ' ', ' ', '0'],
[' ', ' ', ' ', ' ', ' ']
}
This would be the output from the print_board method.
+-----+ || |A 0| || +-----+
Let's take the board above and say we call the add_photon_to_board 5 times. In the first call, the photon passed in has position (0, 1) , the same position as the emitter A . In the second call, the photon passed in has position (1, 1) , and so on, all the way up to position (4, 1) , which has the same position as the receiver R0 .. The board attribute will now look like this.
[
[' ', ' ', ' ', ' ', ' '],
['A', '.', '.', '.', '0'],
[' ', ' ', ' ', ' ', ' ']
}
Notice that it does not replace either the emitter or receiver on the board. This would now be the output from the print_board method.
+-----+ || |A...0| || +-----+
5. Laser Circuit
You will need to implement an adder and getter method for the photons.
When we get to running the circuit, the general idea is that each time a photon moves, we call the add_photon_to_board method to update our board and show the photon in its new position, but we'll worry
about that later.
If you've implemented everything above then congratulations, you have completed Part 1! You have implemented the basic building blocks for running a circuit, now it's just a matter of putting everything together.
Part 2 will focus on implementing the LaserCircuit methods to run the circuit. This is possible as it has knowledge of all components, photons, and the board state. We will then integrate it with our
run program such that from start to end, we can create the user's circuit, and then run it. How fun!
6. Laser Circuit
You will be finishing the implementation of the LaserCircuit class. Implementing these methods will allow you to run a circuit.
6.1 print_emit_photons
The print_emit_photons method is responsible for producing the output of the photon emission
sorted by their symbol. An example is below.
0ns: Emitting photons.
A: 100THz, Down
B: 256THz, Right
Note that we already sorted the circuit's emitters by their symbol in GET-MY-INPUTS so no extra sorting is required.
The method needs to also write the output to a file named emit_photons.out inside /home/output/ . You can assume /home/output/ exists. It should only include the times, not the
header, so the example below shows what the file will contain.
Output Snippet 5 - Output from calling print_emit_photons method.
$ cat /home/output/emit_photons.out
A: 100THz, Down
B: 256THz, Right