diff --git a/environment.yml b/environment.yml index 4a6c489..db603c8 100644 --- a/environment.yml +++ b/environment.yml @@ -18,3 +18,4 @@ dependencies: - sphinx_rtd_theme # for docs - toml>=0.10.2 # core non-optional dependency - tqdm>=4.64.1 # core non-optional dependency + - trimesh>=4.0.7 # for geometric model diff --git a/paseos/actors/actor_builder.py b/paseos/actors/actor_builder.py index 1a53532..b8321c6 100644 --- a/paseos/actors/actor_builder.py +++ b/paseos/actors/actor_builder.py @@ -13,6 +13,7 @@ from ..thermal.thermal_model import ThermalModel from ..power.power_device_type import PowerDeviceType from ..radiation.radiation_model import RadiationModel +from paseos.geometric_model.geometric_model import GeometricModel class ActorBuilder: @@ -305,6 +306,45 @@ def set_position(actor: BaseActor, position: list): actor._position = position logger.debug(f"Setting position {position} on actor {actor}") + @staticmethod + def set_geometric_model( + actor: SpacecraftActor, + mass: float, + vertices=None, + faces=None, + scale: float=1 + + ): + """Define geometry of the spacecraft actor. This is done in the spacecraft body reference frame, and can be + transformed to the inertial/PASEOS reference frame using the reference frane transformations in the attitude + model. When used in the attitude model, the geometric model is in the body reference frame. + + Args: + actor (SpacecraftActor): Actor to update. + mass (float): Mass of the spacecraft in kg + vertices (list): List of all vertices of the mesh in terms of distance (in m) from origin of body frame. + Coordinates of the corners of the object. If not selected, it will default to a cube that can be scaled + by the scale. Uses Trimesh to create the mesh from this and the list of faces. + faces (list): List of the indexes of the vertices of a face. This builds the faces of the satellite by + defining the three vertices to form a triangular face. For a cuboid each face is split into two + triangles. Uses Trimesh to create the mesh from this and the list of vertices. + scale (float): Parameter to scale the cuboid by, defaults to 1 + """ + assert mass > 0, "Mass is > 0" + + actor._mass = mass + geometric_model = GeometricModel( + local_actor=actor, + actor_mass=mass, + vertices=vertices, + faces=faces, + scale=scale + ) + actor._mesh = geometric_model.set_mesh() + actor._moment_of_inertia = geometric_model.find_moment_of_inertia + + + @staticmethod def set_power_devices( actor: SpacecraftActor, diff --git a/paseos/geometric_model/geometric_model.py b/paseos/geometric_model/geometric_model.py new file mode 100644 index 0000000..0d8a185 --- /dev/null +++ b/paseos/geometric_model/geometric_model.py @@ -0,0 +1,100 @@ +from loguru import logger +import trimesh + + +class GeometricModel: + """This model describes the geometry of the spacecraft + Currently it assumes the spacecraft to be a cuboid shape, with width, length and height + """ + + _actor = None + _actor_mesh = None + _actor_center_of_gravity = None + _actor_moment_of_inertia = None + + def __init__( + self, local_actor, actor_mass, vertices=None, faces=None, scale=1 + ) -> None: + """Describes the geometry of the spacecraft, and outputs relevant parameters related to the spacecraft body. + If no vertices or faces are provided, defaults to a cube with unit length sides. This is in the spacecraft body + reference frame, and can be transformed to the inertial/PASEOS reference frame using the transformations in the + attitude model + + Args: + local_actor (SpacecraftActor): Actor to model. + actor_mass (float): Actor's mass in kg. + vertices (list): List of all vertices of the mesh in terms of distance (in m) from origin of body frame. + Coordinates of the corners of the object. If not selected, it will default to a cube that can be scaled + by the scale. Uses Trimesh to create the mesh from this and the list of faces. + faces (list): List of the indexes of the vertices of a face. This builds the faces of the satellite by + defining the three vertices to form a triangular face. For a cuboid each face is split into two + triangles. Uses Trimesh to create the mesh from this and the list of vertices. + scale (float): Parameter to scale the cuboid by, defaults to 1 + """ + logger.trace("Initializing cuboid geometrical model.") + + self._actor = local_actor + self._actor_mass = actor_mass + self.vertices = vertices + self.faces = faces + self.scale = scale + + def set_mesh(self): + """Creates the mesh of the satellite. If no vertices input is given, it defaults to a cuboid scaled by the + scale value. The default without scale values is a cube with 1m sides. This uses the python module Trimesh. + + Returns: + mesh: Trimesh mesh of the satellite + """ + if self.vertices is None: + self.vertices = [ + [-0.5, -0.5, -0.5], + [-0.5, -0.5, 0.5], + [-0.5, 0.5, -0.5], + [-0.5, 0.5, 0.5], + [0.5, -0.5, -0.5], + [0.5, -0.5, 0.5], + [0.5, 0.5, -0.5], + [0.5, 0.5, 0.5], + ] # defines the corners of the mesh, values are in meters, from the origin of the body frame. + self.faces = [ + [0, 1, 3], + [0, 3, 2], + [0, 2, 6], + [0, 6, 4], + [1, 5, 3], + [3, 5, 7], + [2, 3, 7], + [2, 7, 6], + [4, 6, 7], + [4, 7, 5], + [0, 4, 1], + [1, 4, 5], + ] # List of three vertices to form a triangular face of the satellite. Two triangular faces are used + # per side of the cuboid + mesh = trimesh.Trimesh(self.vertices, self.faces) + self._actor_mesh = mesh.apply_scale(self.scale) # Scales the mesh by the scale factor + return self._actor_mesh + + @property + def find_moment_of_inertia(self): + """Gives the moment of inertia of the actor, assuming constant density + + Returns: + np.array: Mass moments of inertia for the actor + + I is the moment of inertia, in the form of [[Ixx Ixy Ixz] + [Iyx Iyy Iyx] + [Izx Izy Izz]] + """ + self._actor_moment_of_inertia = self._actor_mesh.moment_inertia + return self._actor_moment_of_inertia + + def find_center_of_gravity(self): + """Gives the volumetric center of mass of the actor. + + Returns: + np.array: Coordinates of the center of gravity of the mesh + """ + self._actor_center_of_gravity = self._actor_mesh.center_mass + return self._actor_center_of_gravity diff --git a/paseos/tests/actor_builder_test.py b/paseos/tests/actor_builder_test.py index 7dafab5..93bc306 100644 --- a/paseos/tests/actor_builder_test.py +++ b/paseos/tests/actor_builder_test.py @@ -95,3 +95,16 @@ def test_add_comm_device(): assert len(sat1.communication_devices) == 2 assert sat1.communication_devices["dev1"].bandwidth_in_kbps == 10 assert sat1.communication_devices["dev2"].bandwidth_in_kbps == 42 + + +def test_set_geometric_model(): + """Check if we can set the geometry, and if the moments of inertia are calculated correctly""" + _, sat1, _ = get_default_instance() + ActorBuilder.set_geometric_model(sat1, mass=100) + + assert sat1.mass == 100 + assert all(sat1._mesh.center_mass == np.array([0,0,0])) # check the default mesh is centered + assert sat1._mesh.volume == 1 # check the default volume is correct + assert round(sat1._moment_of_inertia[0,0], 4) == 0.1667 # for the default mesh + assert sat1._moment_of_inertia[0,1] == 0.0 # Should be zero if the mass distribution is even + diff --git a/requirements.txt b/requirements.txt index 6ec5b86..9311acd 100644 --- a/requirements.txt +++ b/requirements.txt @@ -7,4 +7,5 @@ pyquaternion>=0.9.9 scikit-spatial>=6.5.0 skyfield>=1.45 toml>=0.10.2 -tqdm>=4.64.1 \ No newline at end of file +tqdm>=4.64.1 +trimesh>=4.0.7 \ No newline at end of file diff --git a/setup.py b/setup.py index 467eab7..b6602ed 100644 --- a/setup.py +++ b/setup.py @@ -22,6 +22,7 @@ "skyfield>=1.45", "toml>=0.10.2", "tqdm>=4.64.1", + "trimesh>=4.0.7", ], classifiers=[ "Development Status :: 3 - Alpha",