diff --git a/objects/calculator.py b/calculator.py similarity index 94% rename from objects/calculator.py rename to calculator.py index d53af76..7d7dc79 100644 --- a/objects/calculator.py +++ b/calculator.py @@ -1,4 +1,3 @@ -from objects.generic import Target, Source import numpy as np # Einheitsvektoren @@ -39,7 +38,7 @@ def agl(a, b): "Get the angle between two vectors. This is always between 0 and 180 degree." return np.round(np.acos(np.dot(a, b)/(np.linalg.norm(a) * np.linalg.norm(b)))/(2 * np.pi) * 360) -def get_angles(source: Source, target: Target): +def get_angles(source, target): """Main function to get the phi and theta angles for a source and a target vector. Both vectors must lie on the front half sphere. Phi is from 0 to 180 where 0 means left when you look at the mirrors. The hardware is bounded between 45 and 135 degree. Thus the here provided angle needs to be subtracted by 45 and then doubled. Theta is from 0 to 90 where 0 means up.""" @@ -51,7 +50,6 @@ def get_angles(source: Source, target: Target): source_theta = agl(rotate(source, 90 - source_phi, 3), unit_z) target_theta = agl(rotate(target, 90 - target_phi, 3), unit_z) - print(target_theta) phi = None theta = None @@ -72,5 +70,4 @@ def get_angles(source: Source, target: Target): else: theta = source_theta + theta_diff/2 - print(phi, theta) return (phi, theta) diff --git a/justfile b/justfile index ed4ed60..39e8744 100644 --- a/justfile +++ b/justfile @@ -1,5 +1,10 @@ +default: sim + sim: uv run python simulation.py +test: + uv run python -m unittest discover -v + sync: rsync -r --exclude=venv ~/solarmotor guest@hahn1.one: diff --git a/main.py b/main.py deleted file mode 100644 index 1e3c83d..0000000 --- a/main.py +++ /dev/null @@ -1,6 +0,0 @@ -def main(): - print("Hello from solarmotor!") - - -if __name__ == "__main__": - main() diff --git a/objects/board.py b/objects/board.py index 6972df6..701b7e7 100644 --- a/objects/board.py +++ b/objects/board.py @@ -3,9 +3,13 @@ from adafruit_servokit import ServoKit class Board: MIN = 500 MAX = 2500 + COVER = 180 + + count = 0 def __init__(self, channels=16, frequency=50): self.channels = channels + self.address = "" # For the future self.frequency = frequency self.kit = ServoKit(channels=channels, frequency=frequency) diff --git a/objects/mirror.py b/objects/mirror.py new file mode 100644 index 0000000..612a2b0 --- /dev/null +++ b/objects/mirror.py @@ -0,0 +1,42 @@ +import numpy as np + +from objects.generic import Source, Target +from objects.motor import Motor + +from calculator import get_angles + +class Mirror: + def __init__(self, world, cluster_x=0, cluster_y=0): + self.world = world + self.cluster_x = cluster_x + self.cluster_y = cluster_y + + # Store the motors + # Need to get first the theta because + # of the ordeing of the cables on the board + self.motor_theta: Motor = Motor(self.world.board) + self.motor_phi: Motor = Motor(self.world.board) + + # Position in un-tilted coordinate system + self.pos = np.array( + [cluster_x * self.world.grid_size, cluster_y * self.world.grid_size, 0.0] + ) + + def get_pos_rotated(self): + return self.world.rotate_point_y(self.pos) + + def set_angle_from_source_target(self, source: Source, target: Target): + "Set the angles of a mirror from global source and target vectors." + + rot_pos = self.get_pos_rotated() + rel_source = source.pos - rot_pos + rel_target = target.pos - rot_pos + + phi, theta = get_angles(rel_source, rel_target) # ty:ignore[unresolved-reference] + + # Update the angles based on the normals in rotated positions + self.motor_phi.set_angle(phi) + self.motor_theta.set_angle(theta) + + def get_angles(self): + return self.motor_phi.angle, self.motor_theta.angle diff --git a/objects/motor.py b/objects/motor.py index faf1fdf..fe12264 100644 --- a/objects/motor.py +++ b/objects/motor.py @@ -5,24 +5,17 @@ from objects.board import Board class Motor: """Model a type of servo motor.""" - # Default vaules for every motor - MAX_PULSE = 2500 - MIN_PULSE = 500 - COVERAGE = 180 # Total degree of freedom in degrees OFFSET = 0 # In degrees a constant to be added SCALE = 1 # Scaling - # Used for ids - count = 0 - def __init__(self, board: Board, angle=0): self.board: Board = board - self.id: int = Motor.count - Motor.count += 1 + self.id: int = Board.count + Board.count += 1 self.angle = angle self.offset = Motor.OFFSET # Fine grained controls over every motor - self.coverage = Motor.COVERAGE + self.coverage = Board.COVER self.scale = Motor.SCALE # Initialization @@ -41,5 +34,5 @@ class Motor: def inc(self, inc): self.angle += inc - self.angle = min(max(self.angle, 0), Motor.COVERAGE) # Clip + self.angle = min(max(self.angle, 0), Board.COVER) # Clip self.set() diff --git a/objects/solar.py b/objects/solar.py deleted file mode 100644 index b79a240..0000000 --- a/objects/solar.py +++ /dev/null @@ -1,70 +0,0 @@ -"""Alle gemessenen Koordinaten der Quelle und der Sonne haben den Ursprung in der linken unteren Ecke des Clusters in einem rechtshaendigen flachen System.""" - -from objects.generic import Source, Target - -import math -import objects.motor as motor -import numpy as np -from objects.calculator import get_angles - - -class Mirror: - def __init__(self, world, cluster_x=0, cluster_y=0): - self.world: World = world - self.cluster_x = cluster_x - self.cluster_y = cluster_y - - # Store the motors - self.theta = motor.Motor(self.world.board) - self.phi = motor.Motor(self.world.board) - - # Position in un-tilted coordinate system - self.pos = np.array( - [cluster_x * self.world.grid_size, cluster_y * self.world.grid_size, 0.0] - ) - - def get_pos_rotated(self): - return self.world.rotate_point_y(self.pos) - - def set_angle_from_source_target(self, source: Source, target: Target): - "Set the angles of a mirror from global source and target vectors." - - rot_pos = self.get_pos_rotated() - rel_source = source.pos - rot_pos - rel_target = target.pos - rot_pos - - phi, theta = get_angles(rel_source, rel_target) - - # Update the angles based on the normals in rotated positions - self.phi.set_angle(phi) - self.theta.set_angle(theta) - - def get_angles(self): - return self.phi.angle, self.theta.angle - - -class World: - def __init__(self, board, tilt_deg=0.0): - self.board = board - - self.grid_size = 10 # In cm - self.tilt_deg = tilt_deg # Tilt of the grid system around y-axis - self.mirrors: list[Mirror] = [] - - def add_mirror(self, mirror): - self.mirrors.append(mirror) - - def update_mirrors_from_source_target(self, source: Source, target: Target): - for mirror in self.mirrors: - mirror.set_angle_from_source_target(source, target) - - def rotate_point_y(self, point): - """Rotate a point around the y-axis by the world's tilt angle.""" - x, y, z = point - theta = math.radians(self.tilt_deg) - cos_t = math.cos(theta) - sin_t = math.sin(theta) - x_rot = x * cos_t + z * sin_t - y_rot = y - z_rot = -x * sin_t + z * cos_t - return np.array([x_rot, y_rot, z_rot]) diff --git a/objects/world.py b/objects/world.py new file mode 100644 index 0000000..6510117 --- /dev/null +++ b/objects/world.py @@ -0,0 +1,55 @@ +""" +Alle gemessenen Koordinaten der Quelle und der Sonne haben den Ursprung in der rechten +oberen Ecke des Clusters in einem rechtshaendigen flachen System. + +Achsen in der Welt mit der z-Achse nach oben. +Alles in cm gemessen. +Der phi Winkel wird zur x-Achse gemessen. +Der thetha Winkel wird zur z-Achse gemessen. + +So sind y und z Koordinaten immer positiv. + + + +x (2,0) (1,0) (0,0) +<-----S----S----S O:z + | + S S S (0,1) + | + v y + +""" + +from objects.generic import Source, Target +from objects.mirror import Mirror + +import numpy as np + +import math + + +class World: + def __init__(self, board, tilt_deg=0.0): + self.board = board + + self.grid_size = 10 # In cm + self.tilt_deg = tilt_deg # Tilt of the grid system around y-axis + self.mirrors: list[Mirror] = [] + + def add_mirror(self, mirror): + self.mirrors.append(mirror) + + def update_mirrors_from_source_target(self, source: Source, target: Target): + for mirror in self.mirrors: + mirror.set_angle_from_source_target(source, target) + + def rotate_point_y(self, point): + """Rotate a point around the y-axis by the world's tilt angle.""" + x, y, z = point + theta = math.radians(self.tilt_deg) + cos_t = np.cos(theta) + sin_t = np.sin(theta) + x_rot = x * cos_t + z * sin_t + y_rot = y + z_rot = -x * sin_t + z * cos_t + return np.array([x_rot, y_rot, z_rot]) diff --git a/simulation.py b/simulation.py index 4c4b0f5..5cd6a40 100644 --- a/simulation.py +++ b/simulation.py @@ -1,26 +1,27 @@ import time -# Solar module for simulation of world -import objects.solar as solar # Modeling of the world - +from objects.generic import Source, Target +from objects.world import World +from objects.mirror import Mirror from objects.board import Board +# Solar module for simulation of world STEP = 10 LOOP_DELAY = 0.005 # In seconds # Testing embedding the mirrors in the world board = Board() -world = solar.World(board, tilt_deg=0) +world = World(board, tilt_deg=0) HEIGHT = 30 -source = solar.Source(world, pos=(0, 50, 0)) -target = solar.Target(world, pos=(0, 50, 0)) +source = Source(world, pos=(0, 50, 0)) +target = Target(world, pos=(0, 50, 0)) # Create mirrors in a 3x2 grid for x in range(2): for y in range(1): - mirror = solar.Mirror(world, cluster_x=x, cluster_y=y) + mirror = Mirror(world, cluster_x=x, cluster_y=y) world.add_mirror(mirror) world.update_mirrors_from_source_target(source, target) @@ -34,10 +35,10 @@ def print_status(): a = 1 t = time.time() -world.mirrors[0].phi.set_angle(180) -world.mirrors[0].theta.set_angle(180) -world.mirrors[1].phi.set_angle(0) -world.mirrors[1].theta.set_angle(0) +world.mirrors[0].motor_theta.set_angle(180) +world.mirrors[0].motor_phi.set_angle(180) +world.mirrors[1].motor_phi.set_angle(0) +world.mirrors[1].motor_theta.set_angle(0) print_status() diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/tests/__init__.py @@ -0,0 +1 @@ + diff --git a/tests/test_calculator.py b/tests/test_calculator.py new file mode 100644 index 0000000..4e05e46 --- /dev/null +++ b/tests/test_calculator.py @@ -0,0 +1,37 @@ +import unittest +import numpy as np +import calculator + + +class TestCalculator(unittest.TestCase): + def test_proj(self): + vec = np.array([123, 325, 1]) + self.assertEqual(np.array([123, 0, 0]).all(), calculator.proj(vec, 1).all()) + + vec = np.array([234, -2134, 12]) + self.assertEqual(np.array([-2134, 0, 0]).all(), calculator.proj(vec, 2).all()) + + vec = np.array([-21, 34, 82]) + self.assertEqual(np.array([0, 0, 82]).all(), calculator.proj(vec, 3).all()) + + def test_rotate(self): + vec = np.array([1, 0, 0]) + self.assertEqual(np.array([0, 1, 0]).all(), calculator.rotate(vec).all()) + + def test_agl(self): + vec1 = np.array([1, 0, 0]) + vec2 = np.array([1, 0, 0]) + self.assertEqual(0, calculator.agl(vec1, vec2)) + + vec1 = np.array([1, 0, 0]) + vec2 = np.array([0, 1, 0]) + self.assertEqual(90, calculator.agl(vec1, vec2)) + + def test_get_angles(self): + source = np.array([0, 50, 0]) + target = np.array([0, 50, 0]) + self.assertEqual((90, 90), calculator.get_angles(source, target)) + + +if __name__ == "__main__": + unittest.main()