compas-dev / compas

Core packages of the COMPAS framework.
https://compas.dev/compas/
MIT License
307 stars 107 forks source link

Custom data from json #1089

Closed kurt-rhee closed 1 year ago

kurt-rhee commented 1 year ago

Describe the bug I am attempting to create custom classes of objects which are an extension of compas.geometry objects and then serialize them to json. These are basically a geometry object(s) and some meta data about the object. These object serialize to json when I call bay.to_json, but fail to validate and also fail to read from_json.

class Bay(Data):
    """
    A bay is a collection of modules sharing the same torque tube.  Note that trackers and bays in TRACE use a
    right-handed cartesian coordinate system with +x pointing East, +y pointing North and +z pointing towards
    the sky.  Industry software such as pvlib and pvsyst use a left-handed coordinate system.
    """
    def __init__(
            self,
            bay_id: str = 'example',
            num_modules: int = 0,
            azimuth: float = 0.0,
            axis_tilt: float = 0.0,
            rotation_angle: int = 0,
            lcs: Frame = Frame([0, 0, 0], [1, 0, 0], [0, 1, 0]),
            lcs_polygon_flat: Polygon = Polygon([[0, 0, 0]])
    ):
        """
        Args:
            bay_id: unique identifier for tracker
            num_modules:  number of pv modules in this bay
            azimuth:  angle rotated about +z axis in a right-handed cartesian frame of reference
            rotation_angle:  angle rotated about the torque tube axis (+y local frame of reference)
            lcs:  local coordinate system frame of reference
            lcs_polygon_flat: geometry object representative of the bay with zero degrees of rotation
            wcs_polygon_rotated:  geometry object representative of the bay with rotation in world coordinate system
        """
        # --- save base information ---
        super().__init__()

        # --- calculations ---
        #  calculate transformation of world and local coordinate systems
        to_wcs = Transformation.from_frame_to_frame(
            Frame.worldXY(),
            lcs
        )

        # calculate rotated polygon
        rotation_about_y = Rotation.from_axis_and_angle(
            [0, 1, 0],
            math.radians(rotation_angle)
        )
        lcs_polygon_rotated = lcs_polygon_flat.transformed(rotation_about_y)

        # load
        self.bay_id = bay_id
        self.num_modules = num_modules
        self.azimuth = azimuth
        self.axis_tilt = axis_tilt
        self.rotation_angle = rotation_angle
        self.lcs = lcs
        self.lcs_polygon_flat = lcs_polygon_flat
        self.wcs_polygon_rotated = lcs_polygon_rotated.transformed(to_wcs)

    @property
    def data(self):
        return {
            'bay_id': self.bay_id,
            'num_modules': self.num_modules,
            'azimuth': self.azimuth,
            'axis_tilt': self.axis_tilt,
            'rotation_angle': self.rotation_angle,
            'lcs': self.lcs,
            'lcs_polygon_flat': self.lcs_polygon_flat,
            'wcs_polygon_rotated': self.wcs_polygon_rotated
        }

    @data.setter
    def data(self, data):
        self.bay_id = data['bay_id']
        self.num_modules = data['num_modules']
        self.azimuth = data['azimuth']
        self.axis_tilt = data['axis_tilt']
        self.rotation_angle = data['rotation_angle']
        self.lcs = data['lcs']
        self.lcs_polygon_flat = data['lcs_polygon_flat']
        self.wcs_polygon_rotated = data['wcs_polygon_rotated']

To Reproduce Steps to reproduce the behavior:

  1. Context python
  2. See above
  3. I can provide a json file if needed
  4. See error

Expected behavior I would expect for the Bay.from_json(file_location) to create a bay object.

Screenshots image

Desktop (please complete the following information):

Additional context Add any other context about the problem here.

yck011522 commented 1 year ago

Just a small suggestion, it would be good to include a script that is runnable for someone else to debug. For example including all the imports in the beginning and a entry point to the script at the end. Not just the class.

Your lcs_polygon_flat should not have only one point. That is an invalid Polygon and it causes the index out of range.

from compas.data import Data
from compas.geometry import *
import math

class Bay(Data):
    """
    A bay is a collection of modules sharing the same torque tube.  Note that trackers and bays in TRACE use a
    right-handed cartesian coordinate system with +x pointing East, +y pointing North and +z pointing towards
    the sky.  Industry software such as pvlib and pvsyst use a left-handed coordinate system.
    """
    def __init__(
            self,
            bay_id: str = 'example',
            num_modules: int = 0,
            azimuth: float = 0.0,
            axis_tilt: float = 0.0,
            rotation_angle: int = 0,
            lcs: Frame = Frame([0, 0, 0], [1, 0, 0], [0, 1, 0]),
            lcs_polygon_flat: Polygon = Polygon([[0, 0, 0], [1, 0, 0], [1, 1, 0], [0, 1, 0]])
    ):
        """
        Args:
            bay_id: unique identifier for tracker
            num_modules:  number of pv modules in this bay
            azimuth:  angle rotated about +z axis in a right-handed cartesian frame of reference
            rotation_angle:  angle rotated about the torque tube axis (+y local frame of reference)
            lcs:  local coordinate system frame of reference
            lcs_polygon_flat: geometry object representative of the bay with zero degrees of rotation
            wcs_polygon_rotated:  geometry object representative of the bay with rotation in world coordinate system
        """
        # --- save base information ---
        super().__init__()

        # --- calculations ---
        #  calculate transformation of world and local coordinate systems
        to_wcs = Transformation.from_frame_to_frame(
            Frame.worldXY(),
            lcs
        )

        # calculate rotated polygon
        rotation_about_y = Rotation.from_axis_and_angle(
            [0, 1, 0],
            math.radians(rotation_angle)
        )
        lcs_polygon_rotated = lcs_polygon_flat.transformed(rotation_about_y)

        # load
        self.bay_id = bay_id
        self.num_modules = num_modules
        self.azimuth = azimuth
        self.axis_tilt = axis_tilt
        self.rotation_angle = rotation_angle
        self.lcs = lcs
        self.lcs_polygon_flat = lcs_polygon_flat
        self.wcs_polygon_rotated = lcs_polygon_rotated.transformed(to_wcs)

    @property
    def data(self):
        return {
            'bay_id': self.bay_id,
            'num_modules': self.num_modules,
            'azimuth': self.azimuth,
            'axis_tilt': self.axis_tilt,
            'rotation_angle': self.rotation_angle,
            'lcs': self.lcs,
            'lcs_polygon_flat': self.lcs_polygon_flat,
            'wcs_polygon_rotated': self.wcs_polygon_rotated
        }

    @data.setter
    def data(self, data):
        self.bay_id = data['bay_id']
        self.num_modules = data['num_modules']
        self.azimuth = data['azimuth']
        self.axis_tilt = data['axis_tilt']
        self.rotation_angle = data['rotation_angle']
        self.lcs = data['lcs']
        self.lcs_polygon_flat = data['lcs_polygon_flat']
        self.wcs_polygon_rotated = data['wcs_polygon_rotated']

if __name__ == "__main__":
    b = Bay()
    print(b.data)

    # Serialization and Deserialize
    b.to_json('test.json', pretty=True)
    c = Bay.from_json('test.json')

    print(c.data)

See if this is what you want. The json looks like this.

    "axis_tilt": 0.0,
    "azimuth": 0.0,
    "bay_id": "example",
    "lcs": {
        "dtype": "compas.geometry/Frame",
        "value": {
            "point": [
                0.0,
                0.0,
                0.0
            ],
            "xaxis": [
                1.0,
                0.0,
                0.0
            ],
            "yaxis": [
                0.0,
                1.0,
                0.0
            ]
        }
    },
    "lcs_polygon_flat": {
        "dtype": "compas.geometry/Polygon",
        "value": {
            "points": [
                [
                    0.0,
                    0.0,
                    0.0
                ],
                [
                    1.0,
                    0.0,
                    0.0
                ],
                [
                    1.0,
                    1.0,
                    0.0
                ],
                [
                    0.0,
                    1.0,
                    0.0
                ]
            ]
        }
    },
    "num_modules": 0,
    "rotation_angle": 0,
    "wcs_polygon_rotated": {
        "dtype": "compas.geometry/Polygon",
        "value": {
            "points": [
                [
                    0.0,
                    0.0,
                    0.0
                ],
                [
                    1.0,
                    0.0,
                    0.0
                ],
                [
                    1.0,
                    1.0,
                    0.0
                ],
                [
                    0.0,
                    1.0,
                    0.0
                ]
            ]
        }
    }
}

print out looks like this

{'bay_id': 'example', 'num_modules': 0, 'azimuth': 0.0, 'axis_tilt': 0.0, 'rotation_angle': 0, 'lcs': Frame(Point(0.000, 0.000, 0.000), Vector(1.000, 0.000, 0.000), Vector(0.000, 1.000, 0.000)), 'lcs_polygon_flat': Polygon([Point(0.000, 0.000, 0.000), Point(1.000, 0.000, 0.000), Point(1.000, 1.000, 0.000), Point(0.000, 1.000, 0.000)]), 'wcs_polygon_rotated': Polygon([Point(0.000, 0.000, 0.000), Point(1.000, 0.000, 0.000), Point(1.000, 1.000, 0.000), Point(0.000, 1.000, 0.000)])}
{'bay_id': 'example', 'num_modules': 0, 'azimuth': 0.0, 'axis_tilt': 0.0, 'rotation_angle': 0, 'lcs': Frame(Point(0.000, 0.000, 0.000), Vector(1.000, 0.000, 0.000), Vector(0.000, 1.000, 0.000)), 'lcs_polygon_flat': Polygon([Point(0.000, 0.000, 0.000), Point(1.000, 0.000, 0.000), Point(1.000, 1.000, 0.000), Point(0.000, 1.000, 0.000)]), 'wcs_polygon_rotated': Polygon([Point(0.000, 0.000, 0.000), Point(1.000, 0.000, 0.000), Point(1.000, 1.000, 0.000), Point(0.000, 1.000, 0.000)])}
kurt-rhee commented 1 year ago

Hello,

Thank you for your help. I am able to get this working on this minimal example, but it my larger script where bay objects are nested inside of tracker objects, tracker objects are nested inside block objects and block objects are nested inside of site objects I can't seem to get this to work.

Is there a limited amount of nested structures that compas will allow?

kurt-rhee commented 1 year ago

A couple more data points:

site
<dom.objects.site.Site object at 0x7fec73ec60a0>

test = Site.from_data(site.data)

test
<dom.objects.site.Site object at 0x7fec4ee87340>

test.blocks[0].trackers[0].bays[0].data
{'bay_id': 0, 'num_modules': 6.0, 'azimuth': 0.0, 'axis_tilt': 0.784329445275794, 'rotation_angle': 0, 'lcs': Frame(Point(1107102.183, 253520.619, 65.197), Vector(1.000, 0.000, 0.000), Vector(-0.000, 1.000, 0.014)), 'lcs_polygon_flat': Polygon([Point(-0.500, 0.000, 0.100), Point(0.500, 0.000, 0.100), Point(0.500, 8.161, 0.100), Point(-0.500, 8.161, 0.100)]), 'wcs_polygon_rotated': Polygon([Point(1107101.683, 253520.618, 65.297), Point(1107102.683, 253520.618, 65.297), Point(1107102.683, 253528.778, 65.409), Point(1107101.683, 253528.778, 65.409)])}
kurt-rhee commented 1 year ago

I upgraded to compas 1.1.7 and followed the 1.1.7 documentation and fixed all of this. Thank you for your help!