Digitizing Plant Layout Data

There are serveral possible ways to digitize the plant’s combiner layout data. Here is a partial overview of one way to do it using the open-source geographic information system (GIS) tool QGIS [1, 2]. There are also commercial GIS tools, as well as web-based tools like https://geojson.io/ that could possibly work.

NOTE: This demonstration uses an arbitrary example plant for illustration purposes. The plants used in later demos are not included here to protect their anonymity.

[1] https://qgis.org/en/site/

[2] https://github.com/qgis/QGIS

Manually outlining combiners in QGIS

NOTE: If your project design package includes shapefiles or other GIS-compatible data that includes combiners, you should start with that dataset as an alternative to this workflow. If you have PDF drawings, it is possible to add them as a layer in QGIS, georeference them, and then use them to trace combiner boundaries, but that is outside the scope of this demo. If you’ve already got your plant outlined in a GeoJSON format, you can jump ahead to Converting GeoJSON Data.

Assuming you don’t already have digitized/georeferenced data for you combiners, you can use QGIS to manually outline combiners. Start by creating a new project and adding a QuickMapServices layer, like Google Hybrid or Bing Satellite.

17d5a5a605464019bdba1b79bd51c866

Then zoom to your plant and add a new shapefile layer with Layer > Create Layer > New Shapefile Layer…

a09ca2e6fcb348409acc2a2ab1586e56

Give the layer a name, select Polygon as the goemetry type, and then add fields that can be used to filter and identify combiners. I chose to create asset_type and asset_name fields, where an asset type could be “combiner” or “inverter”, and asset names could be things like “CMB-001-01”.

2f0797262fda41818d381b66464de8f7

Then draw the feature polygons and assign values for the asset types and names.

NOTE: QGIS has the ability to copy/paste features, lock angles, and other functionality that could help here, but that is outside the scope of this demo.

c0cc1bc7a63e4b7c9ff6d12b2641cc36

Once you are done adding combiners, right-click the layer and select Export > Save Feature As…:

2071bce2e493411fa15fa50e073c1002

And then save as a GeoJSON file:

732b780ef95e433584df12294323e754

NOTE: Digitizing/georeferencing solar plant hardware is commonly done for aerial infrared (thermographic) scanning. Commercial providers of those services may be able to digitize your solar plants at a reasonable cost, and more efficiently than someone that is new to these processes.

Converting GeoJSON data

Now that we have a GeoJSON file, we can use Python to convert it to relative coordinates in meters.

[1]:
# we need geopandas and matplotlib, which are not automatically installed, so we'll install them if they're not present.
try:
    import geopandas as gpd
except ImportError:
    !pip install geopandas
    import geopandas as gpd

try:
    import matplotlib
except ImportError:
    !pip install matplotlib

import pandas as pd

[notice] A new release of pip is available: 23.2.1 -> 24.2
[notice] To update, run: python.exe -m pip install --upgrade pip
Collecting geopandas
  Obtaining dependency information for geopandas from https://files.pythonhosted.org/packages/c4/64/7d344cfcef5efddf9cf32f59af7f855828e9d74b5f862eddf5bfd9f25323/geopandas-1.0.1-py3-none-any.whl.metadata
  Downloading geopandas-1.0.1-py3-none-any.whl.metadata (2.2 kB)
Requirement already satisfied: numpy>=1.22 in d:\code\python\solarspatialtools\venv\lib\site-packages (from geopandas) (1.26.4)
Collecting pyogrio>=0.7.2 (from geopandas)
  Obtaining dependency information for pyogrio>=0.7.2 from https://files.pythonhosted.org/packages/c3/fa/45efa8c96744ddd92c3ce3a80ddba8512954cc7c5407876e2ff2ffea0c10/pyogrio-0.9.0-cp312-cp312-win_amd64.whl.metadata
  Downloading pyogrio-0.9.0-cp312-cp312-win_amd64.whl.metadata (3.9 kB)
Requirement already satisfied: packaging in d:\code\python\solarspatialtools\venv\lib\site-packages (from geopandas) (24.1)
Requirement already satisfied: pandas>=1.4.0 in d:\code\python\solarspatialtools\venv\lib\site-packages (from geopandas) (2.2.2)
Requirement already satisfied: pyproj>=3.3.0 in d:\code\python\solarspatialtools\venv\lib\site-packages (from geopandas) (3.6.1)
Collecting shapely>=2.0.0 (from geopandas)
  Obtaining dependency information for shapely>=2.0.0 from https://files.pythonhosted.org/packages/d4/c3/e98e3eb9f06def32b8e2454ab718cafb99149f023dff023e257125132d6e/shapely-2.0.5-cp312-cp312-win_amd64.whl.metadata
  Downloading shapely-2.0.5-cp312-cp312-win_amd64.whl.metadata (7.2 kB)
Requirement already satisfied: python-dateutil>=2.8.2 in d:\code\python\solarspatialtools\venv\lib\site-packages (from pandas>=1.4.0->geopandas) (2.9.0.post0)
Requirement already satisfied: pytz>=2020.1 in d:\code\python\solarspatialtools\venv\lib\site-packages (from pandas>=1.4.0->geopandas) (2024.1)
Requirement already satisfied: tzdata>=2022.7 in d:\code\python\solarspatialtools\venv\lib\site-packages (from pandas>=1.4.0->geopandas) (2024.1)
Requirement already satisfied: certifi in d:\code\python\solarspatialtools\venv\lib\site-packages (from pyogrio>=0.7.2->geopandas) (2024.7.4)
Requirement already satisfied: six>=1.5 in d:\code\python\solarspatialtools\venv\lib\site-packages (from python-dateutil>=2.8.2->pandas>=1.4.0->geopandas) (1.16.0)
Downloading geopandas-1.0.1-py3-none-any.whl (323 kB)
   ---------------------------------------- 0.0/323.6 kB ? eta -:--:--
   ---- ---------------------------------- 41.0/323.6 kB 960.0 kB/s eta 0:00:01
   ------------------------ --------------- 194.6/323.6 kB 2.4 MB/s eta 0:00:01
   ---------------------------------------- 323.6/323.6 kB 2.8 MB/s eta 0:00:00
Downloading pyogrio-0.9.0-cp312-cp312-win_amd64.whl (15.9 MB)
   ---------------------------------------- 0.0/15.9 MB ? eta -:--:--
   -- ------------------------------------- 1.0/15.9 MB 21.1 MB/s eta 0:00:01
   ----- ---------------------------------- 2.2/15.9 MB 23.0 MB/s eta 0:00:01
   ------ --------------------------------- 2.7/15.9 MB 19.4 MB/s eta 0:00:01
   ----------- ---------------------------- 4.4/15.9 MB 23.5 MB/s eta 0:00:01
   ------------- -------------------------- 5.6/15.9 MB 23.7 MB/s eta 0:00:01
   --------------- ------------------------ 6.0/15.9 MB 21.4 MB/s eta 0:00:01
   ------------------- -------------------- 7.8/15.9 MB 23.6 MB/s eta 0:00:01
   ---------------------- ----------------- 8.9/15.9 MB 23.7 MB/s eta 0:00:01
   ----------------------- ---------------- 9.2/15.9 MB 21.8 MB/s eta 0:00:01
   --------------------------- ------------ 11.0/15.9 MB 24.2 MB/s eta 0:00:01
   ------------------------------ --------- 12.2/15.9 MB 23.4 MB/s eta 0:00:01
   --------------------------------- ------ 13.4/15.9 MB 25.2 MB/s eta 0:00:01
   ------------------------------------ --- 14.4/15.9 MB 25.1 MB/s eta 0:00:01
   ---------------------------------------  15.5/15.9 MB 23.4 MB/s eta 0:00:01
   ---------------------------------------- 15.9/15.9 MB 24.2 MB/s eta 0:00:00
Downloading shapely-2.0.5-cp312-cp312-win_amd64.whl (1.4 MB)
   ---------------------------------------- 0.0/1.4 MB ? eta -:--:--
   ------------------------------ --------- 1.1/1.4 MB 23.1 MB/s eta 0:00:01
   ---------------------------------------- 1.4/1.4 MB 23.1 MB/s eta 0:00:00
Installing collected packages: shapely, pyogrio, geopandas
Successfully installed geopandas-1.0.1 pyogrio-0.9.0 shapely-2.0.5

Read in the GeoJSON file that we created with QGIS and preview the resulting DataFrame, which uses latitude and longitude coordinates in degrees:

[2]:
data = gpd.read_file('data/digitized_plant.geojson')
data.head()
[2]:
id asset_type asset_name geometry
0 None combiner CMB-001-01 MULTIPOLYGON (((-83.67303 33.68023, -83.67303 ...
1 None combiner CMB-001-02 MULTIPOLYGON (((-83.67282 33.68006, -83.67282 ...
2 None combiner CMB-001-03 MULTIPOLYGON (((-83.67261 33.67975, -83.67261 ...
3 None combiner CMB-001-04 MULTIPOLYGON (((-83.6724 33.67946, -83.6724 33...
4 None combiner CMB-001-05 MULTIPOLYGON (((-83.6722 33.67916, -83.6722 33...

We want to create an anonymized dataset, where the distances are preserved, but the absolute position is relative to the plant. Set the origin latitude and longitude using the minimum latitude and longitude values (Southwest corner of the plant, in this case):

[3]:
origin_long = data.geometry.get_coordinates().x.min()
origin_lat = data.geometry.get_coordinates().y.min()

We’ll then project by defining a custom coordinate reference system (CRS) using meters and based on that origin point. The proj=aeqd part of the projection creates an azimuthal equidistant projection, which is useful for measuring distances from a single point.

[4]:
custom_crs = "+proj=aeqd +lat_0=" + \
    str(origin_lat) + " +lon_0=" + \
    str(origin_long) + " +datum=WGS84 +units=m"

# Convert to custom coordinate system
data_utm = data.to_crs(custom_crs)

A quick plot to preview the combiners in the resulting data:

[5]:
data_utm.loc[data_utm['asset_type']=='combiner'].plot(aspect='equal')
[5]:
<Axes: >
../_images/demos_plant_digitization_example_11_1.png

And write the results to a new GeoJSON file that uses meters relative to the plant corner for coordinates. The location of the plant is now anonymized.

[6]:
# Write new file
data_utm.to_file('data/digitized_plant_meters.geojson', driver='GeoJSON')
data_utm.head()
[6]:
id asset_type asset_name geometry
0 None combiner CMB-001-01 MULTIPOLYGON (((99.546 241.961, 99.546 224.268...
1 None combiner CMB-001-02 MULTIPOLYGON (((118.433 222.72, 118.433 191.75...
2 None combiner CMB-001-03 MULTIPOLYGON (((138.209 189.103, 138.21 158.58...
3 None combiner CMB-001-04 MULTIPOLYGON (((157.097 156.37, 157.097 125.40...
4 None combiner CMB-001-05 MULTIPOLYGON (((176.429 123.416, 176.429 105.7...

Exporting Data for Use by SolarSpatialTools

The data is now converted to a relative rectilinear coordinate system. One data item that we frequently need for analyses (e.g. field) is the centroid of these positions. We can extract that data and save it to a convenient format, like CSV or h5.

Since the shapes in the GeoJSON file are polygons, we can get the centroid of each shape using the centroid attribute. We will store this in a standard pandas DataFrame, indexed by the asset_name field that we created. We will also filter on asset_type of combiner, so that we don’t include centroids of other assets that may have been digitized.

[7]:
data_boundaries = data_utm.loc[data_utm['asset_type']=='combiner']
data_centroids = pd.DataFrame(
    data_boundaries.centroid.get_coordinates().values,
    index=data_utm.asset_name, columns=['E', 'N']
    )
data_centroids.head()
[7]:
E N
asset_name
CMB-001-01 76.504393 232.462541
CMB-001-02 63.430049 206.657679
CMB-001-03 69.549150 173.841325
CMB-001-04 78.919717 140.997993
CMB-001-05 88.699994 114.623431

You could now save the data as desired. Examples of CSV and h5 formats are shown below.

[8]:
data_centroids.to_csv(f'data/digitized_plant_centroids.csv')
data_centroids.to_hdf(f'data/digitized_plant_centroids.h5', key='utm', mode='a', append=True)