mirror of
https://gitlab.ewi.tudelft.nl/ee2l1/2025-2026/A.K.03.git
synced 2025-12-12 16:00:56 +01:00
515 lines
24 KiB
Plaintext
515 lines
24 KiB
Plaintext
{
|
||
"cells": [
|
||
{
|
||
"cell_type": "markdown",
|
||
"metadata": {},
|
||
"source": [
|
||
"\n",
|
||
"[Table of Contents](0_Table_of_Contents.ipynb)"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "markdown",
|
||
"metadata": {},
|
||
"source": [
|
||
"# Chapter 2: Module 0 - Before We Start\n",
|
||
"\n",
|
||
"**Contents:**\n",
|
||
"* [Initial Software Installations](#initial-software-installation)\n",
|
||
"* [Object Oriented Programming](#object-oriented-programming-in-ip3)\n",
|
||
"* [Programming in Python](#programming-in-python)\n",
|
||
"* [The Car Simulator](#the-car-simulator)\n",
|
||
"\n",
|
||
"Before beginning the project, this chapter provids guidelines on installing the required software, an introduction to Object-Oriented Programming (OOP), quick tips on Python programming, and an overview of the car simulator used in the modules. Carefully read through this introduction and complete the setup preparation before starting the modules."
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "markdown",
|
||
"metadata": {},
|
||
"source": [
|
||
"## Initial software Installation\n",
|
||
"Before we can start doing anything, you need to set up your programming environment in Python. We have provided a step by step guide that walks you through installing Python, Visual Studio Code, and all the necessary dependencies for your project. You can find them here:\n",
|
||
"\n",
|
||
"1. [Installation Mac](../appendix/0_Installation_Mac.ipynb)\n",
|
||
"2. [Installation Windows](../appendix/0_Installation_Windows.ipynb)"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "markdown",
|
||
"metadata": {},
|
||
"source": [
|
||
"## Object-Oriented Programming in IP3\n",
|
||
"\n",
|
||
"Throughout the project, you will write over 20 functions and a couple hundred lines of code. As you will find, there is exponential difficulty with the increase in project size. Writing many functions is part of the assignment, but the challenge is testing these functions and getting them to work together. You must communicate with the car and interpret the data received, accurately find the car's location, plan how to drive to the final destination, generate steering commands, and adjust the plan while you drive and discover objects. \n",
|
||
"\n",
|
||
"This tutorial introduces object-oriented programming (OOP) concepts in Python, which is a programming method that provides flexibility. It is highly recommended that you learn how to use OOP, which will be useful throughout this project and future projects. The modules will provide code snippets, assuming you understand basic OOP concepts to enhance the functionality of the KITT car project.\n",
|
||
"\n",
|
||
"### Class and object\n",
|
||
"In OOP, a class is a template or a set of instructions for creating objects. On the other hand, an object is a specific instance or realization of that template. To illustrate, let's make a blueprint for KITT using a class."
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": null,
|
||
"metadata": {},
|
||
"outputs": [],
|
||
"source": [
|
||
"class KITT:\n",
|
||
" def __init__(self, model, color):\n",
|
||
" self.model = model\n",
|
||
" self.color = color\n",
|
||
" self.is_engine_on = False\n",
|
||
"\n",
|
||
" def start_engine(self):\n",
|
||
" self.is_engine_on = True\n",
|
||
" print(f\"{self.model}'s engine started.\")\n",
|
||
"\n",
|
||
" def stop_engine(self):\n",
|
||
" self.is_engine_on = False\n",
|
||
" print(f\"{self.model}'s engine stopped.\")"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "markdown",
|
||
"metadata": {},
|
||
"source": [
|
||
"**Attributes:** These are characteristics or properties that describe the state of an object. In the real world, consider attributes as the features defining an object. In the class definition, attributes are represented by variables. In the example, we have defined a class 'KITT' with attributes 'model', 'color', and 'is_engine_on'. Which are just some characteristic of KITT we could define.\n",
|
||
"\n",
|
||
"**Methods:** These are functions that define the behavior of an object. They represent the actions that an object can perform. Methods are defined within the class and are used to manipulate the object's state (attributes) or perform some action associated with the object. Continuing with the car example, the methods 'start_engine' and 'stop_engine' control the car's engine.\n",
|
||
"\n",
|
||
"**Self:** Within the class definition, `self' is used to refer to the object. \n",
|
||
"\n",
|
||
"**The __init__ method** is a special method called the constructor. It is automatically called when a new object is created. In this method, we initialize the *model*, *color*, and *is_engine_on* variables to the values passed.\n",
|
||
"\n",
|
||
"We can now make some instances of the class KITT. We call these objects."
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": null,
|
||
"metadata": {},
|
||
"outputs": [],
|
||
"source": [
|
||
"if __name__ == \"__main__\":\n",
|
||
" car1 = KITT(\"TRX4\", \"black\") # Make the first instance of KITT\n",
|
||
" car2 = KITT(\"Rustler\", \"red\") # Make the second instance of KITT\n",
|
||
"\n",
|
||
" car2.color = \"blue\" # Change the color of car2\n",
|
||
" \n",
|
||
" car1.start_engine() # Start the engine of car1 \n",
|
||
" print(car1.is_engine_on) # Output: \"True\"\n",
|
||
" print(car1.model) # Output: \"TRX4\"\n",
|
||
" print(car1.color) # Output: \"black\""
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "markdown",
|
||
"metadata": {},
|
||
"source": [
|
||
"First, two instances of KITT are made. They are made from the same KITT class (template) but are a different model and color. These are stored as attributes to the instance (also called object). It is possible to change an attribute of an object after it has been made. In this example, the color of the second car is changed to blue. The state of the engine is also stored as an attribute. It is defaulted to False (the engine is off). Now, the engine of car1 is started. When checked, car1 outputs that the engine is now set to on. \n",
|
||
"\n",
|
||
"### Method vs Function\n",
|
||
"\n",
|
||
"In Python, both methods and functions are blocks of code that can be called upon to perform specific tasks. However, there are fundamental differences between the two.\n",
|
||
"\n",
|
||
"#### Function:\n",
|
||
"\n",
|
||
"A function is a block of code that is defined outside of a class and can be called independently. It takes input arguments (if any), performs some operations, and optionally returns a result. Functions in Python are versatile and can be reused across different parts of a program.\n",
|
||
"\n",
|
||
"#### Method:\n",
|
||
"\n",
|
||
"A method, on the other hand, is a function that is associated with an object. It is defined within a class and operates on the data associated with the class (attributes). Methods are accessed through instances of the class (objects) and can modify the state of the object. The first argument of a method is always the special variable 'self', which refers to the instance of the class."
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": null,
|
||
"metadata": {},
|
||
"outputs": [],
|
||
"source": [
|
||
"class KITT:\n",
|
||
" def __init__(self, model, color):\n",
|
||
" self.model = model\n",
|
||
" self.color = color\n",
|
||
" self.is_engine_on = False\n",
|
||
"\n",
|
||
" def start_engine(self):\n",
|
||
" self.is_engine_on = True\n",
|
||
" print(f\"{self.model}'s engine started.\")\n",
|
||
"\n",
|
||
" def stop_engine(self):\n",
|
||
" self.is_engine_on = False\n",
|
||
" print(f\"{self.model}'s engine stopped.\")\n",
|
||
"\n",
|
||
"def drive():\n",
|
||
" print(\"KITT is driving.\")\n",
|
||
"\n",
|
||
"if __name__ == \"__main__\":\n",
|
||
" car = KITT(\"TRX4\", \"black\") # Create an instance of KITT\n",
|
||
" car.start_engine() # Call the start_engine method\n",
|
||
" drive() # Call the drive function"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "markdown",
|
||
"metadata": {},
|
||
"source": [
|
||
"In this example, 'start_engine' and 'stop_engine' are methods because they are defined within the KITT class and operate on the KITT object's state. On the other hand, 'drive' is a function defined outside of the class and can be called independently.\n",
|
||
"\n",
|
||
"### Hidden and Private variables\n",
|
||
"\n",
|
||
"In object-oriented programming, there are concepts of encapsulation and data hiding, which allow for better control over the accessibility of attributes and methods within a class. This helps in preventing accidental modification of data and ensures the integrity of the class.\n",
|
||
"\n",
|
||
"#### Hidden variables\n",
|
||
"\n",
|
||
"In Python, variables and methods can be hidden from the outside world using a single underscore (_) prefix. Although they can still be accessed, it indicates to other programmers that these elements are intended for internal use and should be treated as such."
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": null,
|
||
"metadata": {},
|
||
"outputs": [],
|
||
"source": [
|
||
"class KITT:\n",
|
||
" def __init__(self, model, color):\n",
|
||
" self.model = model\n",
|
||
" self.color = color\n",
|
||
" self._is_engine_on = False # Hidden variable\n",
|
||
"\n",
|
||
" def start_engine(self):\n",
|
||
" self._is_engine_on = True\n",
|
||
" print(f\"{self.model}'s engine started.\")\n",
|
||
"\n",
|
||
" def stop_engine(self):\n",
|
||
" self._is_engine_on = False\n",
|
||
" print(f\"{self.model}'s engine stopped.\")"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "markdown",
|
||
"metadata": {},
|
||
"source": [
|
||
"In this modified version of the KITT class, the variable *_is_engine_on* is prefixed with a single underscore. This indicates that it's a hidden variable. While it can still be accessed from outside the class, the underscore serves as a convention to signal that it's intended for internal use.\n",
|
||
"\n",
|
||
"#### Private variables\n",
|
||
"\n",
|
||
"Python also supports the concept of private variables, which are variables that cannot be accessed or modified from outside the class. They are denoted by a double underscore (__) prefix."
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": null,
|
||
"metadata": {},
|
||
"outputs": [],
|
||
"source": [
|
||
"class KITT:\n",
|
||
" def __init__(self, model, color):\n",
|
||
" self.model = model\n",
|
||
" self.color = color\n",
|
||
" self.__is_locked = True # Private variable\n",
|
||
"\n",
|
||
" def lock_car(self):\n",
|
||
" self.__is_locked = True\n",
|
||
" print(f\"{self.model} is locked.\")\n",
|
||
"\n",
|
||
" def unlock_car(self):\n",
|
||
" self.__is_locked = False\n",
|
||
" print(f\"{self.model} is unlocked.\")"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "markdown",
|
||
"metadata": {},
|
||
"source": [
|
||
"In this version, the variable *__is_locked* is prefixed with a double underscore, making it a private variable. Private variables cannot be accessed directly from outside the class. Attempting to do so will result in an AttributeError. Special methods should be made to modify these variables called getters and setters.\n",
|
||
"\n",
|
||
"### Getters and Setters\n",
|
||
"\n",
|
||
"In object-oriented programming, getters and setters are methods used to access and modify the private or hidden variables of a class, respectively. They provide controlled access to the class's attributes, allowing for validation and encapsulation of data.\n",
|
||
"\n",
|
||
"#### Getters:\n",
|
||
"\n",
|
||
"Getters are methods used to retrieve the values of private or hidden variables. They provide a way to access the state of an object without directly exposing its attributes."
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": null,
|
||
"metadata": {},
|
||
"outputs": [],
|
||
"source": [
|
||
"class KITT:\n",
|
||
" def __init__(self, model, color):\n",
|
||
" self.model = model\n",
|
||
" self.color = color\n",
|
||
" self.__is_locked = True\n",
|
||
"\n",
|
||
" # Getter for is_locked\n",
|
||
" def is_locked(self):\n",
|
||
" return self.__is_locked"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "markdown",
|
||
"metadata": {},
|
||
"source": [
|
||
"In this modified KITT class, a getter method *is_locked()* is added to retrieve the value of the private variable *__is_locked*.\n",
|
||
"\n",
|
||
"#### Setters:\n",
|
||
"\n",
|
||
"Setters are methods used to modify the values of private or hidden variables. They provide a way to update the state of an object while enforcing validation rules or constraints. For example:"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": null,
|
||
"metadata": {},
|
||
"outputs": [],
|
||
"source": [
|
||
"class KITT:\n",
|
||
" def __init__(self, model, color):\n",
|
||
" self.model = model\n",
|
||
" self.color = color\n",
|
||
" self.__is_locked = True\n",
|
||
"\n",
|
||
" # Getter for is_locked\n",
|
||
" def is_locked(self):\n",
|
||
" return self.__is_locked\n",
|
||
"\n",
|
||
" # Setter for is_locked\n",
|
||
" def set_lock_status(self):\n",
|
||
" if self.__is_locked:\n",
|
||
" self.__is_locked = False\n",
|
||
" print(f\"{self.model} is unlocked.\")\n",
|
||
" else:\n",
|
||
" self.__is_locked = True\n",
|
||
" print(f\"{self.model} is locked.\")"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "markdown",
|
||
"metadata": {},
|
||
"source": [
|
||
"In this updated KITT class, a setter method *set_lock_status()* is added to modify the value of the private variable *__is_locked*. This setter will automatically switch the locked state of the car. If it was unlocked, it locks the car, and vice verso. \n",
|
||
"\n",
|
||
"**Example:**"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": null,
|
||
"metadata": {},
|
||
"outputs": [],
|
||
"source": [
|
||
"if __name__ == \"__main__\":\n",
|
||
" car = KITT(\"TRX4\", \"black\") # Create an instance of KITT\n",
|
||
"\n",
|
||
" # Using getter to check if the car is locked\n",
|
||
" print(car.is_locked()) # Output: True\n",
|
||
"\n",
|
||
" # Using setter to unlock the car\n",
|
||
" car.set_lock_status() # Output: \"TRX4 is unlocked.\""
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "markdown",
|
||
"metadata": {},
|
||
"source": [
|
||
"In this example, first the getter method *is_locked()* is called to check if the car is locked. Then, the setter method *set_lock_status()* is called to change the lock status, because the car when initiated is locked, it is now unlocked."
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "markdown",
|
||
"metadata": {},
|
||
"source": [
|
||
"## Programming in Python\n",
|
||
"\n",
|
||
"### Hints on programming\n",
|
||
"\n",
|
||
"The above tutorial briefly introduced OOP in Python and demonstrated its application in the KITT car project. However, some of these concepts can be abstract when first introduced. Therefore, you should look for more resources as you experiment with OOP.\n",
|
||
"\n",
|
||
"- When writing the code to implement functionalities required for this project, partition the code into separate functions and always include a header block with a function. In this header block, you should briefly describe the function, the required inputs, and what the output will do. Including a changelog with author names and dates is also good practice.\n",
|
||
"- Use meaningful variable names and write many comments so that others can understand what the code is doing.\n",
|
||
"- Define variables for constants such as $F_s$ rather than using numbers throughout the code. That way, you give meaning to that number; if the number changes, you have to change it only at a single location.\n",
|
||
"- Avoid the use of globals. If a function needs a parameter, include it in the function call. If you must use globals, write the variable names in capitals. The risk of using globals is that if their value changes, it affects functions that depend on them while that dependency is hidden.\n",
|
||
"- When writing your code, be sure to decouple each function, test it separately, and briefly report on these tests. If you run into any problems further down the design process, finding where functions might not agree and where your problem could lie will be easier.\n",
|
||
"\n",
|
||
"The test itself should also be in a script, so that you can frequently rerun it in case some of the functions have changed and need to be tested again. \n",
|
||
"- In your report, describe the overall structure of the code and the main variables so that others can quickly find their way into your code.\n",
|
||
"- Test every function in your code! For every 'if' statement in the code, you have two branches to test.\n",
|
||
"\n",
|
||
"### Useful modules\n",
|
||
"\n",
|
||
"**Time measurement in Python** In IP3, accurately measuring time is crucial for various tasks such as determining the duration of events or operations. Python provides the *time* package, which offeers functionality to measure time intervals.\n",
|
||
"\n",
|
||
"To measure time intervals using *time* package, follow these steps:"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": null,
|
||
"metadata": {},
|
||
"outputs": [],
|
||
"source": [
|
||
"import time\n",
|
||
"\n",
|
||
"# Record the start time\n",
|
||
"start = time.time()\n",
|
||
"\n",
|
||
"# Perform an operation or task\n",
|
||
"# For example, simulate a computational task\n",
|
||
"for _ in range(1000000):\n",
|
||
" pass\n",
|
||
"\n",
|
||
"# Record the end time\n",
|
||
"end = time.time()\n",
|
||
"\n",
|
||
"# Calculate the duration of the operation\n",
|
||
"duration = end - start\n",
|
||
"\n",
|
||
"# Print the duration\n",
|
||
"print(f\"The operation lasted for {duration} seconds.\")"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "markdown",
|
||
"metadata": {},
|
||
"source": [
|
||
"In this code snippet, the *time* package was imported. The *time.time()* function returns your computers time in seconds. The *time.time()* is called to record the start time before executing the operation. After completing the operation, the end time is recorded. By subtracting the start time from the end time, the duration of the operation is calculated.\n",
|
||
"\n",
|
||
"**Detecting Keyboard events in Python** In module 1, you will be tasked with controlling the car from your keyboard. For this you will need to detect keyboard events, such as key presses. \n",
|
||
"\n",
|
||
"To detect keyboard events using the *pynpyt* library, follow these steps:"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": null,
|
||
"metadata": {},
|
||
"outputs": [],
|
||
"source": [
|
||
"from pynput import keyboard\n",
|
||
"\n",
|
||
"# Define callback for when a key is pressed\n",
|
||
"def on_press(key):\n",
|
||
" try:\n",
|
||
" print(f'Key {key.char} pressed')\n",
|
||
" except AttributeError:\n",
|
||
" print(f'Special key {key} pressed')\n",
|
||
"\n",
|
||
"# Define callback for when a key is released\n",
|
||
"def on_release(key):\n",
|
||
" print(f'Key {key} released')\n",
|
||
" # Stop listener if 'Esc' key is released\n",
|
||
" if key == keyboard.Key.esc:\n",
|
||
" return False\n",
|
||
"\n",
|
||
"# Setup the listener\n",
|
||
"with keyboard.Listener(on_press=on_press, on_release=on_release) as listener:\n",
|
||
" listener.join()"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "markdown",
|
||
"metadata": {},
|
||
"source": [
|
||
"### Explanation:\n",
|
||
"1. **`on_press` function:** This function is called whenever a key is pressed. It tries to print the character of the key (for normal alphanumeric keys). If it's a special key (like 'Shift', 'Ctrl', etc.), it will print the special key.\n",
|
||
"2. **`on_release` function:** This function is called whenever a key is released. It prints which key was released and stops the listener when the 'Esc' key is released.\n",
|
||
"3. **`keyboard.Listener`:** This is the main object that listens for keyboard events. The `on_press` and `on_release` functions are passed as callbacks.\n",
|
||
"\n",
|
||
"Run the script, and as you press or release keys, it will print the corresponding information to the console. The program will keep running until you press the 'Esc' key."
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "markdown",
|
||
"metadata": {},
|
||
"source": [
|
||
"## KITT Simulator\n",
|
||
"\n",
|
||
"To facilitate the development and testing of your algorithms, we have created a digital model of it. This is a software tool that replicates the behavior of KITT, providing a virtual environment where you can test and refine your code without needing access to the physical car. It simulates the car’s dynamics, including speed, steering, and sensor responses, reacting to the same Python commands that control the real car. This allows you to experiment with different control algorithms and get immediate feedback on how the car would behave, even when you're away from the lab. Switching between the simulator and the physical car is seamless—simply change a single import statement at the top of your code to toggle between them.\n",
|
||
"\n",
|
||
"The car simulator is in the Python package `KITT_Simulator`. Its use is explained in Module 1.\n",
|
||
"\n",
|
||
"Below is already a small demo of what it can do:"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": null,
|
||
"metadata": {},
|
||
"outputs": [],
|
||
"source": [
|
||
"from KITT_Simulator.serial_simulator import Serial\n",
|
||
"import time\n",
|
||
"# Open serial port\n",
|
||
"serial = Serial('/dev/ttyUSB0', 115200)\n",
|
||
"\n",
|
||
"# Wait for one second to allow for computer processing delay\n",
|
||
"time.sleep(1)\n",
|
||
"# Set speed and direction\n",
|
||
"serial.write(b'M163\\n')\n",
|
||
"serial.write(b'D150\\n')\n",
|
||
"\n",
|
||
"time.sleep(2)\n",
|
||
"\n",
|
||
"# Set speed and direction\n",
|
||
"serial.write(b'M162\\n')\n",
|
||
"serial.write(b'D120\\n')\n",
|
||
"\n",
|
||
"time.sleep(2)\n",
|
||
"\n",
|
||
"# Set speed to zero\n",
|
||
"serial.write(b'M150\\n')\n",
|
||
"print(\"Motors are OFF\")\n",
|
||
"\n",
|
||
"time.sleep(5) # Notice the car keeps moving (rolling out till standstill)\n",
|
||
"\n",
|
||
"# Close the connection (important!)\n",
|
||
"serial.close()"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "markdown",
|
||
"metadata": {},
|
||
"source": [
|
||
"### Limitations\n",
|
||
"\n",
|
||
"The simulator is based on the behavior of one specific car. It is important to note that each car has slight variations, which can lead to noticeable differences in performance. Factors like battery status, friction and motor efficiency can affect the accuracy of distance estimations and other dynamics. The simulator is best used for testing basic functionality—ensuring your code compiles, runs correctly, and performs simple tasks. However, the final validation should always be conducted on the real car. Additionally, some features in the simulator are simplified or idealized, such as distance sensors and audio recordings, which are based on generic settings rather than your specific configuration.\n",
|
||
"\n",
|
||
"### How the Car Simulator Works\n",
|
||
"\n",
|
||
"The car simulator consists of several key components that work together to create a realistic and interactive testing environment:\n",
|
||
"\n",
|
||
"1. **Dynamics Simulation Module**: This module models the physical behavior of the car, responding to inputs like throttle and steering. It factors in elements such as acceleration, deceleration, turning radius, and friction, providing realistic feedback on how the car would respond in different situations.\n",
|
||
"2. **Serial and State Communication**: Commands can be sent to the simulator via a serial interface or a shared state file. This enables smooth communication between your Python code and the virtual car.\n",
|
||
"3. **Sensor Simulation**: The simulator mimics the data output from physical sensors, including distance measurements and other feedback. This allows you to develop and test algorithms for tasks like feedback control and obstacle avoidance in a simulated environment.\n",
|
||
"4. **Graphical User Interface (GUI)**: The GUI offers a visual representation of the car’s movements, making it easy to observe its behavior in real time. You can monitor important metrics such as speed, distance traveled, and steering angle directly from the interface.\n",
|
||
"5. **Python Integration**: The simulator is designed to be controlled using Python scripts, just like the actual car. This ensures that the code you develop for the simulator can be easily adapted for use with the physical car, minimizing the transition effort. "
|
||
]
|
||
}
|
||
],
|
||
"metadata": {
|
||
"kernelspec": {
|
||
"display_name": "OpenEdVenv",
|
||
"language": "python",
|
||
"name": "python3"
|
||
},
|
||
"language_info": {
|
||
"codemirror_mode": {
|
||
"name": "ipython",
|
||
"version": 3
|
||
},
|
||
"file_extension": ".py",
|
||
"mimetype": "text/x-python",
|
||
"name": "python",
|
||
"nbconvert_exporter": "python",
|
||
"pygments_lexer": "ipython3",
|
||
"version": "3.12.6"
|
||
}
|
||
},
|
||
"nbformat": 4,
|
||
"nbformat_minor": 2
|
||
}
|