WEC Module

This module contains the Wave Energy Converter (WEC) device and farm abstractions used in WEC-Grid.

Overview

The WEC module provides: - WECDevice: Individual wave energy converter device modeling - WECFarm: Collection and management of multiple WEC devices - WECSimRunner: Integration with WEC-Sim simulation engine

API Reference

WEC-Grid WEC device/farm abstractions

WECDevice dataclass

Individual Wave Energy Converter device with time-series power output data.

Represents a single wave energy converter with simulation results, grid connection parameters, and metadata. Contains time-series power output data from WEC-Sim hydrodynamic simulations for realistic renewable generation modeling.

Attributes:

Name Type Description
name str

Unique device identifier, typically "{model}{sim_id}".

dataframe DataFrame

Primary time-series data for grid integration at 5-minute intervals. Columns: time, p [MW], q [MVAr], base [MVA].

dataframe_full DataFrame

High-resolution simulation data with complete WEC-Sim output including wave elevation and device states.

base float

Base power rating [MVA] for per-unit calculations.

bus_location int

Power system bus number for grid connection.

model str

WEC device model type ("RM3", "LUPA", etc.).

sim_id int

Database simulation identifier for traceability.

Example

power_data = pd.DataFrame({ ... 'p': [2.5, 3.1, 2.8], # MW ... 'q': [0.0, 0.0, 0.0], # MVAr ... 'base': [100.0] * 3 # MVA ... }) device = WECDevice( ... name="RM3_101_0", ... dataframe=power_data, ... base=100.0, ... bus_location=14, ... model="RM3" ... )

Notes
  • Variable power output based on wave conditions
  • Typically operates at unity power factor (zero reactive power)
  • Primary dataframe at 5-minute resolution for grid compatibility
  • Full dataframe contains high-resolution WEC-Sim results
Source code in src/wecgrid/wec/device.py
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
@dataclass
class WECDevice:
    """Individual Wave Energy Converter device with time-series power output data.

    Represents a single wave energy converter with simulation results, grid connection
    parameters, and metadata. Contains time-series power output data from WEC-Sim
    hydrodynamic simulations for realistic renewable generation modeling.

    Attributes:
        name (str): Unique device identifier, typically "{model}_{sim_id}_{index}".
        dataframe (pd.DataFrame): Primary time-series data for grid integration at
            5-minute intervals. Columns: time, p [MW], q [MVAr], base [MVA].
        dataframe_full (pd.DataFrame): High-resolution simulation data with complete
            WEC-Sim output including wave elevation and device states.
        base (float, optional): Base power rating [MVA] for per-unit calculations.
        bus_location (int, optional): Power system bus number for grid connection.
        model (str, optional): WEC device model type ("RM3", "LUPA", etc.).
        sim_id (int, optional): Database simulation identifier for traceability.

    Example:
        >>> power_data = pd.DataFrame({
        ...     'p': [2.5, 3.1, 2.8],  # MW
        ...     'q': [0.0, 0.0, 0.0],  # MVAr
        ...     'base': [100.0] * 3    # MVA
        ... })
        >>> device = WECDevice(
        ...     name="RM3_101_0",
        ...     dataframe=power_data,
        ...     base=100.0,
        ...     bus_location=14,
        ...     model="RM3"
        ... )

    Notes:
        - Variable power output based on wave conditions
        - Typically operates at unity power factor (zero reactive power)
        - Primary dataframe at 5-minute resolution for grid compatibility
        - Full dataframe contains high-resolution WEC-Sim results
    """

    name: str
    dataframe: pd.DataFrame = field(default_factory=pd.DataFrame)
    bus_location: Optional[int] = None
    model: Optional[str] = None
    wec_sim_id: Optional[int] = None

    def __repr__(self) -> str:
        """Return a formatted string representation of the WEC device configuration.

        Provides a hierarchical display of key device parameters for debugging,
        logging, and user information. The format shows essential device
        characteristics including identification, grid connection, and data status.

        Returns:
            str: Formatted multi-line string with device configuration details.
                Includes device name, model type, grid connection parameters,
                simulation metadata, base power rating, and data size in a
                tree-like structure for easy reading.

        Example:
            >>> device = WECDevice(
            ...     name="RM3_101_0",
            ...     model="RM3",
            ...     bus_location=14,
            ...     sim_id=101,
            ...     dataframe=power_data  # 288 rows
            ... )
            >>> print(device)
            WECDevice:
            ├─ name: 'RM3_101_0'
            ├─ model: 'RM3'
            ├─ bus_location: 14
            ├─ sim_id: 101
            └─ rows: 288

        Display Format:
            - **Tree structure**: Uses Unicode box-drawing characters
            - **Device identification**: Name and model type in quotes
            - **Grid parameters**: Bus location and simulation ID as integers
            - **Data size**: Number of time-series data points

        Information Categories:
            - **Identity**: Device name (typically includes model and index)
            - **Type**: WEC model for hydrodynamic characteristics
            - **Grid connection**: Bus location for electrical network modeling
            - **Simulation link**: Database ID for traceability
            - **Data status**: Time-series length for validation

        Use Cases:
            - **Interactive debugging**: Quick device configuration inspection
            - **Jupyter notebooks**: Clean display in research environments
            - **Logging output**: Structured device information for log files
            - **Data validation**: Verify device setup and data availability
            - **Farm inspection**: Review individual devices in large collections

        Notes:
            - Name and model shown in quotes to distinguish strings
            - Row count reflects primary dataframe length (grid integration data)
            - Missing values displayed as None for optional parameters
            - Unicode characters may not display properly in all terminals
            - Format consistent with WECFarm.__repr__() for visual coherence

        See Also:
            WECFarm.__repr__: Similar formatting for farm-level display
        """
        return f"""WECDevice:
    ├─ name: {self.name!r}
    ├─ model: {self.model!r}
    ├─ bus_location: {self.bus_location}
    ├─ sim_id: {self.wec_sim_id}
    └─ rows: {len(self.dataframe)}
    """

WECFarm

Collection of Wave Energy Converter devices at a common grid connection.

Manages multiple identical WEC devices sharing a grid connection bus. Aggregates device power outputs and coordinates time-series data for power system integration studies.

Attributes:

Name Type Description
farm_name str

Human-readable farm identifier.

database

Database interface for WEC simulation data.

time

Time manager for simulation synchronization.

wec_sim_id int

Database simulation ID for WEC data retrieval.

model str

WEC device model type (e.g., "RM3").

bus_location int

Grid bus number for farm connection.

connecting_bus int

Network topology connection bus.

id str

Unique generator identifier for power system integration.

size int

Number of identical WEC devices in farm.

config Dict

Configuration parameters for the farm.

wec_devices List[WECDevice]

Collection of individual WEC devices.

BASE float

Base power rating [MVA] for per-unit calculations.

Example

farm = WECFarm( ... farm_name="Oregon Coast Farm", ... database=db, ... time=time_mgr, ... sim_id=101, ... model="RM3", ... bus_location=14, ... size=5 ... ) total_power = farm.power_at_snapshot(timestamp)

Notes
  • All devices use identical power profiles from WEC-Sim data
  • Power scales linearly with farm size
  • Requires WEC-Sim simulation data in database
  • Base power typically 100 MVA for utility-scale installations
TODO
  • Add heterogeneous device support for different models
  • Implement smart farm control and optimization
Source code in src/wecgrid/wec/farm.py
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
class WECFarm:
    """Collection of Wave Energy Converter devices at a common grid connection.

    Manages multiple identical WEC devices sharing a grid connection bus.
    Aggregates device power outputs and coordinates time-series data for
    power system integration studies.

    Attributes:
        farm_name (str): Human-readable farm identifier.
        database: Database interface for WEC simulation data.
        time: Time manager for simulation synchronization.
        wec_sim_id (int): Database simulation ID for WEC data retrieval.
        model (str): WEC device model type (e.g., "RM3").
        bus_location (int): Grid bus number for farm connection.
        connecting_bus (int): Network topology connection bus.
        id (str): Unique generator identifier for power system integration.
        size (int): Number of identical WEC devices in farm.
        config (Dict): Configuration parameters for the farm.
        wec_devices (List[WECDevice]): Collection of individual WEC devices.
        BASE (float): Base power rating [MVA] for per-unit calculations.

    Example:
        >>> farm = WECFarm(
        ...     farm_name="Oregon Coast Farm",
        ...     database=db,
        ...     time=time_mgr,
        ...     sim_id=101,
        ...     model="RM3",
        ...     bus_location=14,
        ...     size=5
        ... )
        >>> total_power = farm.power_at_snapshot(timestamp)

    Notes:
        - All devices use identical power profiles from WEC-Sim data
        - Power scales linearly with farm size
        - Requires WEC-Sim simulation data in database
        - Base power typically 100 MVA for utility-scale installations

    TODO:
        - Add heterogeneous device support for different models
        - Implement smart farm control and optimization
    """

    def __init__(
        self,
        farm_name: str,
        database,
        time: Any,
        wec_sim_id: int,
        bus_location: int,
        connecting_bus: int = 1,
        gen_name: str = "",
        size: int = 1,
        farm_id: int = None,
        sbase: float = 100.0,
        scaling_factor: float = 1.0,
    ):
        """Initialize WEC farm with specified configuration.

        Args:
            farm_name (str): Human-readable WEC farm identifier.
            database: Database interface for WEC simulation data access.
            time: Time management object for simulation synchronization.
            wec_sim_id (int): Database simulation ID for WEC data retrieval.
            bus_location (int): Grid bus number for farm connection.
            connecting_bus (int, optional): Network topology connection bus. Defaults to 1.
            gen_name (str, optional): Generator name for power system integration. Defaults to ''.
            size (int, optional): Number of WEC devices in farm. Defaults to 1.
            farm_id (int, optional): Unique farm identifier. Defaults to None.
            sbase (float, optional): Base power rating [MVA] for per-unit calculations. Defaults to 100.0.
            scaling_factor (float, optional): Linear power scaling factor for aggregated output. Defaults to 1.0.

        Raises:
            RuntimeError: If WEC simulation data not found in database.
            ValueError: If database query returns empty results.

        Example:
            >>> farm = WECFarm(
            ...     farm_name="Newport Array",
            ...     database=db,
            ...     time=time_mgr,
            ...     wec_sim_id=101,
            ...     bus_location=14,
            ...     size=5
            ... )

        Notes:
            - Creates identical WECDevice objects for all farm devices
            - Retrieves WEC-Sim data from database using wec_sim_id
            - Sets up per-unit base power from simulation data
        """

        self.farm_name: str = farm_name
        self.database = database  # TODO make this a WECGridDB data type
        self.time = time  # todo might need to update time to be SimulationTime type
        self.wec_sim_id: int = wec_sim_id
        self.model: str = ""
        self.bus_location: int = bus_location
        self.connecting_bus: int = (
            connecting_bus  # todo this should default to swing bus
        )
        self.farm_id: int = farm_id
        self.size: int = size
        self.config: Dict = None
        self.wec_devices: List[WECDevice] = []
        self.sbase: float = sbase
        self.scaling_factor: float = scaling_factor
        self.gen_name = gen_name
        # todo don't need the base here anymore
        # todo: add bus voltage.
        # todo: connecting line

        self._prepare_farm()

    def __repr__(self) -> str:
        """Return a formatted string representation of the WEC farm configuration.

        Provides a hierarchical display of key farm parameters for debugging,
        logging, and user information. The format is designed for readability
        and includes essential configuration details.

        Returns:
            str: Formatted multi-line string with farm configuration details.
                Includes farm name, size, model, grid connections, simulation ID,
                and base power rating in a tree-like structure.

        Example:
            >>> farm = WECFarm("Test Farm", db, time_mgr, 101, "RM3", 14, size=5)
            >>> print(farm)
            WECFarm:
            ├─ name: 'Test Farm'
            ├─ size: 5
            ├─ model: 'RM3'
            ├─ bus_location: 14
            ├─ connecting_bus: 1
            └─ wec_sim_id: 101

            Base: 100.0 MVA

        Display Format:
            - **Tree structure**: Uses Unicode box-drawing characters
            - **Key parameters**: Farm name, device count, model type
            - **Grid connections**: Bus locations for power system integration
            - **Simulation link**: Database simulation identifier
            - **Base power**: MVA rating for per-unit calculations

        Use Cases:
            - **Interactive debugging**: Quick farm configuration inspection
            - **Logging output**: Structured information for log files
            - **Jupyter notebooks**: Clean display in research environments
            - **Configuration validation**: Verify farm setup parameters

        Notes:
            - Size displays actual number of created devices (len(wec_devices))
            - Farm name shown in quotes to distinguish from other identifiers
            - Base power extracted from WEC simulation data during initialization
            - Unicode characters may not display properly in all terminals
        """
        return f"""WECFarm:
        ├─ name: {self.farm_name!r}
        ├─ size: {len(self.wec_devices)}
        ├─ model: {self.model!r}
        ├─ bus_location: {self.bus_location}
        ├─ connecting_bus: {self.connecting_bus}
        └─ sim_id: {self.wec_sim_id}

        Base: {self.sbase} MVA

    """

    def _prepare_farm(self):
        """Load WEC simulation data from database and create individual device objects.

        Internal method that handles the core initialization logic for the WEC farm.
        Validates database content, retrieves simulation results, processes time
        indexing, and instantiates the specified number of WEC device objects.

        Returns:
            None: Populates self.wec_devices with configured WECDevice objects.

        Raises:
            RuntimeError: If WEC simulation data not found or loading fails.
                Provides guidance on running WEC-Sim simulations first.
            ValueError: If database returns empty or invalid data.
            KeyError: If required columns missing from simulation data.

        Data Loading Process:
            1. **Table Existence Check**: Verify simulation tables exist in database
            2. **Grid Data Retrieval**: Load downsampled results for power system integration
            3. **Full Data Retrieval**: Load high-resolution results for detailed analysis
            4. **Time Index Creation**: Generate pandas datetime index for time series
            5. **Base Power Extraction**: Get MVA rating from simulation data
            6. **Device Instantiation**: Create specified number of WECDevice objects

        Database Table Schema:
            **Integration table** (`WECSIM_{model}_{sim_id}`):
            - time: Simulation time [s]
            - p: Active power output [MW]
            - q: Reactive power output [MVAr] (typically zero)
            - base: Base power rating [MVA]

            **Full resolution table** (`WECSIM_{model}_{sim_id}_full`):
            - time: Simulation time [s] (high frequency)
            - p: Active power output [MW]
            - eta: Wave surface elevation [m]
            - Additional WEC-Sim variables

        Time Series Processing:
            - **5-minute intervals**: Standard grid integration time step
            - **Datetime indexing**: Converts simulation seconds to timestamp format
            - **Start time alignment**: Uses time manager's configured start time
            - **Pandas integration**: Creates DataFrame index for efficient querying

        Device Creation:
            Each WECDevice configured with:
            - **Unique naming**: "{model}_{sim_id}_{index}" pattern
            - **Shared data**: Copy of power time series for each device
            - **Common parameters**: Bus location, base power, model type
            - **Individual objects**: Separate instance for potential customization

        Error Handling:
            - **Missing tables**: Clear error message with WEC-Sim guidance
            - **Empty data**: Validation of successful data retrieval
            - **Data integrity**: Checks for required columns and valid values
            - **Graceful failure**: Informative error messages for troubleshooting

        Performance Considerations:
            - **Single query**: Minimizes database access for efficiency
            - **Data copying**: Each device gets independent DataFrame copy
            - **Memory usage**: Scales with farm size and simulation duration
            - **Time indexing**: One-time conversion for all devices

        Notes:
            - Called automatically during farm initialization
            - Assumes WEC-Sim simulation completed successfully
            - Base power typically 100 MVA for utility-scale installations
            - All devices share identical power profiles (homogeneous farm)
            - TODO: Add data validation and integrity checks
            - TODO: Support for custom time intervals beyond 5 minutes

        See Also:
            WECDevice: Individual device objects created by this method
            WECSimRunner: Generates the simulation data loaded here
            WECGridTime: Provides time configuration for indexing
        """
        # First get model type from wec_simulations table using wec_sim_id
        model_query = "SELECT model_type FROM wec_simulations WHERE wec_sim_id = ?"
        model_result = self.database.query(model_query, params=(self.wec_sim_id,))

        if not model_result:
            raise RuntimeError(
                f"[Farm] No simulation metadata found for wec_sim_id={self.wec_sim_id}"
            )

        # Update self.model from database
        if isinstance(model_result, list) and len(model_result) > 0:
            self.model = (
                model_result[0][0]
                if isinstance(model_result[0], (list, tuple))
                else model_result[0]["model_type"]
            )
        else:
            raise RuntimeError(
                f"[Farm] Invalid model data returned for wec_sim_id={self.wec_sim_id}"
            )

        # print(f"[Farm] Loaded WEC model '{self.model}' for simulation ID {self.wec_sim_id}")

        # Check if WEC simulation data exists in new schema
        sim_check_query = "SELECT wec_sim_id FROM wec_simulations WHERE wec_sim_id = ?"
        sim_result = self.database.query(sim_check_query, params=(self.wec_sim_id,))

        if not sim_result:
            raise RuntimeError(
                f"[Farm] No WEC simulation found for wec_sim_id={self.wec_sim_id}. Run WEC-SIM first."
            )

        # Load WEC power data from new database schema
        power_query = """
            SELECT time_sec as time, p_w as p, q_var as q, wave_elevation_m as eta 
            FROM wec_power_results 
            WHERE wec_sim_id = ? 
            ORDER BY time_sec
        """
        df_full = self.database.query(
            power_query, params=(self.wec_sim_id,), return_type="df"
        )

        if df_full is None or df_full.empty:
            raise RuntimeError(
                f"[Farm] No WEC power data found for wec_sim_id={self.wec_sim_id}"
            )

        df_full.p = self.scaling_factor * df_full.p  # scale active power
        df_full.q = self.scaling_factor * df_full.q  # scale reactive power

        # Downsample the full resolution data for grid integration
        df_downsampled = self.down_sample(df_full, self.time.delta_time)

        # Apply time index at 5 min resolution using start time
        df_downsampled["snapshots"] = pd.date_range(
            start=self.time.start_time,
            periods=df_downsampled.shape[0],
            freq=self.time.freq,
        )

        # apply the snapshots
        df_downsampled.set_index("snapshots", inplace=True)

        # Convert Watts to per-unit of sbase MVA
        # WEC data is stored in Watts, need to convert to MW then to per-unit
        # Conversion: Watts → MW (÷1e6) → per-unit (÷sbase_MVA)
        df_downsampled["p"] = df_downsampled["p"] / (self.sbase * 1e6)  # Watts to pu
        df_downsampled["q"] = df_downsampled["q"] / (self.sbase * 1e6)  # Watts to pu

        for i in range(self.size):
            name = f"{self.model}_{self.wec_sim_id}_{i}"
            device = WECDevice(
                name=name,
                dataframe=df_downsampled.copy(),  # Use downsampled data for grid integration
                bus_location=self.bus_location,
                model=self.model,
                wec_sim_id=self.wec_sim_id,
            )
            self.wec_devices.append(device)

    def down_sample(
        self, wec_df: pd.DataFrame, new_sample_period: float, timeshift: int = 0
    ) -> pd.DataFrame:
        """Downsample WEC time-series data to a coarser time resolution.

        Converts high-frequency WEC simulation data to lower frequency suitable for
        power system integration studies. Averages data over specified time windows
        to maintain energy conservation while reducing computational overhead.

        Based on MATLAB DownSampleTS function with pandas DataFrame implementation.

        Args:
            wec_df (pd.DataFrame): Original high-frequency WEC data with 'time' column.
                Must contain time series data with consistent time step.
            new_sample_period (float): New sampling period [seconds] for downsampled data.
                Typically 300s (5 minutes) for grid integration studies.
            timeshift (int, optional): Time alignment option. Defaults to 0.
                - 0: Samples at end of averaging period
                - 1: Samples centered within averaging period

        Returns:
            pd.DataFrame: Downsampled DataFrame with same columns as input.
                Time column adjusted to new sampling frequency.
                Data columns contain averaged values over sampling windows.

        Raises:
            ValueError: If new_sample_period is smaller than original time step.
            KeyError: If 'time' column not found in input DataFrame.

        Example:
            >>> # Downsample 0.1s WEC data to 5-minute intervals
            >>> df_original = pd.DataFrame({
            ...     'time': np.arange(0, 1000, 0.1),  # 0.1s timestep
            ...     'p': np.random.rand(10000),        # Power data
            ...     'eta': np.random.rand(10000)       # Wave elevation
            ... })
            >>> df_downsampled = farm.down_sample(df_original, 300.0)  # 5min
            >>> print(f"Original: {len(df_original)} points")
            >>> print(f"Downsampled: {len(df_downsampled)} points")
            Original: 10000 points
            Downsampled: 33 points

        Averaging Process:
            1. **Calculate sample ratio**: How many original points per new point
            2. **Determine new time grid**: Based on sample period and alignment
            3. **Window averaging**: Mean value over each time window
            4. **Energy conservation**: Maintains total energy content

        Time Alignment Options:
            **timeshift = 0** (End-aligned):
            - New timestamps at end of averaging window
            - t_new = [T, 2T, 3T, ...] where T = new_sample_period

            **timeshift = 1** (Center-aligned):
            - New timestamps at center of averaging window
            - t_new = [T/2, T+T/2, 2T+T/2, ...] where T = new_sample_period

        Data Processing:
            - **First window**: Averages from start to first sample point
            - **Subsequent windows**: Averages over fixed-width windows
            - **Missing data**: Handles partial windows at end of series
            - **Column preservation**: Maintains all non-time columns

        Performance Considerations:
            - **Memory efficient**: Uses vectorized pandas operations
            - **Flexible windows**: Handles non-integer sample ratios
            - **Large datasets**: Suitable for long WEC simulations
            - **Numerical stability**: Robust averaging implementation

        Grid Integration Usage:
            - **PSS®E studies**: 5-minute resolution for stability analysis
            - **Economic dispatch**: Hourly or 15-minute intervals
            - **Load forecasting**: Daily or weekly aggregation
            - **Resource assessment**: Monthly or seasonal averages

        Wave Energy Applications:
            - **Power smoothing**: Reduces high-frequency fluctuations
            - **Grid compliance**: Matches utility data requirements
            - **Forecast validation**: Aligns with meteorological predictions
            - **Storage sizing**: Determines energy storage requirements

        Notes:
            - Preserves energy content through proper averaging
            - Original time step must be consistent (fixed timestep)
            - New sample period should be multiple of original timestep
            - Returns DataFrame with same structure as input
            - Time column values updated to new sampling frequency

        See Also:
            _prepare_farm: Uses this method for WEC data preprocessing
            WECGridTime: Provides target sampling frequencies
            pandas.DataFrame.resample: Alternative pandas resampling method
        """
        if "time" not in wec_df.columns:
            raise KeyError("DataFrame must contain 'time' column for downsampling")

        # Calculate original time step (assuming fixed timestep)
        time_values = wec_df["time"].values
        if len(time_values) < 2:
            return wec_df.copy()  # Return original if too few points

        old_dt = time_values[1] - time_values[0]

        if new_sample_period <= old_dt:
            raise ValueError(
                f"New sample period ({new_sample_period}s) must be larger than original timestep ({old_dt}s)"
            )

        # Calculate sampling parameters
        t_sample = int(new_sample_period / old_dt)  # Points per new sample
        new_sample_size = int((time_values[-1] - time_values[0]) / new_sample_period)

        if new_sample_size <= 0:
            return wec_df.copy()  # Return original if downsampling not possible

        # Create new time grid
        if timeshift == 1:
            # Center-aligned timestamps
            new_times = np.arange(
                new_sample_period / 2,
                new_sample_size * new_sample_period + new_sample_period / 2,
                new_sample_period,
            )
        else:
            # End-aligned timestamps
            new_times = np.arange(
                new_sample_period,
                (new_sample_size + 1) * new_sample_period,
                new_sample_period,
            )

        # Ensure we don't exceed the original time range
        new_times = new_times[new_times <= time_values[-1]]
        new_sample_size = len(new_times)

        # Initialize downsampled DataFrame
        downsampled_data = {"time": new_times}

        # Downsample each data column (excluding time)
        data_columns = [col for col in wec_df.columns if col != "time"]

        for col in data_columns:
            downsampled_values = np.zeros(new_sample_size)

            for i in range(new_sample_size):
                if i == 0:
                    # First window: from start to first sample point
                    start_idx = 0
                    end_idx = min(t_sample, len(wec_df))
                else:
                    # Subsequent windows: fixed-width windows
                    start_idx = (i - 1) * t_sample
                    end_idx = min(i * t_sample, len(wec_df))

                if start_idx < len(wec_df) and end_idx > start_idx:
                    downsampled_values[i] = wec_df[col].iloc[start_idx:end_idx].mean()
                else:
                    downsampled_values[i] = 0.0  # Handle edge cases

            downsampled_data[col] = downsampled_values

        return pd.DataFrame(downsampled_data)

    def power_at_snapshot(self, timestamp: pd.Timestamp) -> float:
        """Calculate total farm power output at a specific simulation time.

        Aggregates active power output from all WEC devices in the farm at the
        specified timestamp. This method provides the primary interface for
        power system integration, enabling time-varying renewable generation
        modeling in grid simulations.

        Args:
            timestamp (pd.Timestamp): Simulation time to query for power output.
                Must exist in the device DataFrame time index. Typically corresponds
                to grid simulation snapshots at 5-minute intervals.

        Returns:
            float: Total active power output from all farm devices in per-unit on
                the farm's ``sbase``. Sum of individual device outputs at the
                specified time. Returns 0.0 if no valid data available at
                timestamp.

        Raises:
            KeyError: If timestamp not found in device data index.
            AttributeError: If device DataFrame not properly initialized.

        Example:
            >>> # Get power at specific simulation time
            >>> timestamp = pd.Timestamp("2023-01-01 12:00:00")
            >>> power_pu = farm.power_at_snapshot(timestamp)
            >>> print(f"Farm output at noon: {power_pu:.4f} pu")
            Farm output at noon: 0.1575 pu

            >>> # Time series power extraction
            >>> time_series = []
            >>> for snapshot in time_manager.snapshots:
            ...     power_pu = farm.power_at_snapshot(snapshot)
            ...     time_series.append(power_pu)
            >>>
            >>> import matplotlib.pyplot as plt
            >>> plt.plot(time_manager.snapshots, time_series)
            >>> plt.ylabel("Farm Power Output [pu]")

        Power Aggregation:
            - **Linear summation**: Total = Σ(device_power[i] at timestamp)
            - **Homogeneous devices**: All devices have identical power profiles
            - **Realistic scaling**: Based on actual WEC device physics
            - **Wave correlation**: Devices respond to same ocean conditions

        Data Requirements:
            - **Valid timestamp**: Must exist in device DataFrame index
            - **Initialized devices**: All WECDevice objects must be properly created
            - **Power column**: Device data must contain "p" column for active power
            - **Time alignment**: Timestamp must match grid simulation schedule

        Error Handling:
            - **Missing data warning**: Prints warning for devices with no data
            - **Graceful degradation**: Continues calculation with available devices
            - **Zero fallback**: Returns 0.0 if no devices have valid data
            - **Timestamp validation**: Checks for existence in device index

        Performance Considerations:
            - **O(n) complexity**: Scales linearly with number of devices
            - **DataFrame lookup**: Efficient pandas indexing for time queries
            - **Memory efficiency**: No data copying, direct access to device data
            - **Repeated calls**: Suitable for time-series iteration

        Grid Integration Usage:
            - **PSS®E integration**: Provides generator output at each time step
            - **PyPSA integration**: Supplies renewable generation time series
            - **Load flow studies**: Time-varying injection for stability analysis
            - **Economic dispatch**: Variable renewable generation modeling

        Wave Energy Characteristics:
            - **Intermittent output**: Power varies with wave conditions
            - **Predictable patterns**: Follows ocean wave statistics
            - **Seasonal variation**: Higher output in winter storm seasons
            - **Capacity factor**: Typically 20-40% for ocean wave resources

        Notes:
            - Output is in per-unit on the farm's ``sbase``; multiply by
              ``sbase`` for MW
            - Power output includes WEC device efficiency and control effects
            - All devices share identical profiles (same wave field assumption)
            - Negative power values possible during reactive conditions
            - Zero output during calm conditions or device maintenance
            - Farm total limited by grid connection capacity

        See Also:
            WECDevice.dataframe: Individual device power time series
            Engine.simulate: Uses this method for grid integration
            WECGridPlotter.plot_wec_analysis: Visualizes farm power output
        """
        total_power = 0.0
        for device in self.wec_devices:
            if (
                device.dataframe is not None
                and not device.dataframe.empty
                and timestamp in device.dataframe.index
            ):
                power = device.dataframe.at[timestamp, "p"]
                total_power += power
            else:
                print(f"[WARNING] Missing data for {device.name} at {timestamp}")
        return total_power

down_sample(wec_df, new_sample_period, timeshift=0)

Downsample WEC time-series data to a coarser time resolution.

Converts high-frequency WEC simulation data to lower frequency suitable for power system integration studies. Averages data over specified time windows to maintain energy conservation while reducing computational overhead.

Based on MATLAB DownSampleTS function with pandas DataFrame implementation.

Parameters:

Name Type Description Default
wec_df DataFrame

Original high-frequency WEC data with 'time' column. Must contain time series data with consistent time step.

required
new_sample_period float

New sampling period [seconds] for downsampled data. Typically 300s (5 minutes) for grid integration studies.

required
timeshift int

Time alignment option. Defaults to 0. - 0: Samples at end of averaging period - 1: Samples centered within averaging period

0

Returns:

Type Description
DataFrame

pd.DataFrame: Downsampled DataFrame with same columns as input. Time column adjusted to new sampling frequency. Data columns contain averaged values over sampling windows.

Raises:

Type Description
ValueError

If new_sample_period is smaller than original time step.

KeyError

If 'time' column not found in input DataFrame.

Example

Downsample 0.1s WEC data to 5-minute intervals

df_original = pd.DataFrame({ ... 'time': np.arange(0, 1000, 0.1), # 0.1s timestep ... 'p': np.random.rand(10000), # Power data ... 'eta': np.random.rand(10000) # Wave elevation ... }) df_downsampled = farm.down_sample(df_original, 300.0) # 5min print(f"Original: {len(df_original)} points") print(f"Downsampled: {len(df_downsampled)} points") Original: 10000 points Downsampled: 33 points

Averaging Process
  1. Calculate sample ratio: How many original points per new point
  2. Determine new time grid: Based on sample period and alignment
  3. Window averaging: Mean value over each time window
  4. Energy conservation: Maintains total energy content
Time Alignment Options

timeshift = 0 (End-aligned): - New timestamps at end of averaging window - t_new = [T, 2T, 3T, ...] where T = new_sample_period

timeshift = 1 (Center-aligned): - New timestamps at center of averaging window - t_new = [T/2, T+T/2, 2T+T/2, ...] where T = new_sample_period

Data Processing
  • First window: Averages from start to first sample point
  • Subsequent windows: Averages over fixed-width windows
  • Missing data: Handles partial windows at end of series
  • Column preservation: Maintains all non-time columns
Performance Considerations
  • Memory efficient: Uses vectorized pandas operations
  • Flexible windows: Handles non-integer sample ratios
  • Large datasets: Suitable for long WEC simulations
  • Numerical stability: Robust averaging implementation
Grid Integration Usage
  • PSS®E studies: 5-minute resolution for stability analysis
  • Economic dispatch: Hourly or 15-minute intervals
  • Load forecasting: Daily or weekly aggregation
  • Resource assessment: Monthly or seasonal averages
Wave Energy Applications
  • Power smoothing: Reduces high-frequency fluctuations
  • Grid compliance: Matches utility data requirements
  • Forecast validation: Aligns with meteorological predictions
  • Storage sizing: Determines energy storage requirements
Notes
  • Preserves energy content through proper averaging
  • Original time step must be consistent (fixed timestep)
  • New sample period should be multiple of original timestep
  • Returns DataFrame with same structure as input
  • Time column values updated to new sampling frequency
See Also

_prepare_farm: Uses this method for WEC data preprocessing WECGridTime: Provides target sampling frequencies pandas.DataFrame.resample: Alternative pandas resampling method

Source code in src/wecgrid/wec/farm.py
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
def down_sample(
    self, wec_df: pd.DataFrame, new_sample_period: float, timeshift: int = 0
) -> pd.DataFrame:
    """Downsample WEC time-series data to a coarser time resolution.

    Converts high-frequency WEC simulation data to lower frequency suitable for
    power system integration studies. Averages data over specified time windows
    to maintain energy conservation while reducing computational overhead.

    Based on MATLAB DownSampleTS function with pandas DataFrame implementation.

    Args:
        wec_df (pd.DataFrame): Original high-frequency WEC data with 'time' column.
            Must contain time series data with consistent time step.
        new_sample_period (float): New sampling period [seconds] for downsampled data.
            Typically 300s (5 minutes) for grid integration studies.
        timeshift (int, optional): Time alignment option. Defaults to 0.
            - 0: Samples at end of averaging period
            - 1: Samples centered within averaging period

    Returns:
        pd.DataFrame: Downsampled DataFrame with same columns as input.
            Time column adjusted to new sampling frequency.
            Data columns contain averaged values over sampling windows.

    Raises:
        ValueError: If new_sample_period is smaller than original time step.
        KeyError: If 'time' column not found in input DataFrame.

    Example:
        >>> # Downsample 0.1s WEC data to 5-minute intervals
        >>> df_original = pd.DataFrame({
        ...     'time': np.arange(0, 1000, 0.1),  # 0.1s timestep
        ...     'p': np.random.rand(10000),        # Power data
        ...     'eta': np.random.rand(10000)       # Wave elevation
        ... })
        >>> df_downsampled = farm.down_sample(df_original, 300.0)  # 5min
        >>> print(f"Original: {len(df_original)} points")
        >>> print(f"Downsampled: {len(df_downsampled)} points")
        Original: 10000 points
        Downsampled: 33 points

    Averaging Process:
        1. **Calculate sample ratio**: How many original points per new point
        2. **Determine new time grid**: Based on sample period and alignment
        3. **Window averaging**: Mean value over each time window
        4. **Energy conservation**: Maintains total energy content

    Time Alignment Options:
        **timeshift = 0** (End-aligned):
        - New timestamps at end of averaging window
        - t_new = [T, 2T, 3T, ...] where T = new_sample_period

        **timeshift = 1** (Center-aligned):
        - New timestamps at center of averaging window
        - t_new = [T/2, T+T/2, 2T+T/2, ...] where T = new_sample_period

    Data Processing:
        - **First window**: Averages from start to first sample point
        - **Subsequent windows**: Averages over fixed-width windows
        - **Missing data**: Handles partial windows at end of series
        - **Column preservation**: Maintains all non-time columns

    Performance Considerations:
        - **Memory efficient**: Uses vectorized pandas operations
        - **Flexible windows**: Handles non-integer sample ratios
        - **Large datasets**: Suitable for long WEC simulations
        - **Numerical stability**: Robust averaging implementation

    Grid Integration Usage:
        - **PSS®E studies**: 5-minute resolution for stability analysis
        - **Economic dispatch**: Hourly or 15-minute intervals
        - **Load forecasting**: Daily or weekly aggregation
        - **Resource assessment**: Monthly or seasonal averages

    Wave Energy Applications:
        - **Power smoothing**: Reduces high-frequency fluctuations
        - **Grid compliance**: Matches utility data requirements
        - **Forecast validation**: Aligns with meteorological predictions
        - **Storage sizing**: Determines energy storage requirements

    Notes:
        - Preserves energy content through proper averaging
        - Original time step must be consistent (fixed timestep)
        - New sample period should be multiple of original timestep
        - Returns DataFrame with same structure as input
        - Time column values updated to new sampling frequency

    See Also:
        _prepare_farm: Uses this method for WEC data preprocessing
        WECGridTime: Provides target sampling frequencies
        pandas.DataFrame.resample: Alternative pandas resampling method
    """
    if "time" not in wec_df.columns:
        raise KeyError("DataFrame must contain 'time' column for downsampling")

    # Calculate original time step (assuming fixed timestep)
    time_values = wec_df["time"].values
    if len(time_values) < 2:
        return wec_df.copy()  # Return original if too few points

    old_dt = time_values[1] - time_values[0]

    if new_sample_period <= old_dt:
        raise ValueError(
            f"New sample period ({new_sample_period}s) must be larger than original timestep ({old_dt}s)"
        )

    # Calculate sampling parameters
    t_sample = int(new_sample_period / old_dt)  # Points per new sample
    new_sample_size = int((time_values[-1] - time_values[0]) / new_sample_period)

    if new_sample_size <= 0:
        return wec_df.copy()  # Return original if downsampling not possible

    # Create new time grid
    if timeshift == 1:
        # Center-aligned timestamps
        new_times = np.arange(
            new_sample_period / 2,
            new_sample_size * new_sample_period + new_sample_period / 2,
            new_sample_period,
        )
    else:
        # End-aligned timestamps
        new_times = np.arange(
            new_sample_period,
            (new_sample_size + 1) * new_sample_period,
            new_sample_period,
        )

    # Ensure we don't exceed the original time range
    new_times = new_times[new_times <= time_values[-1]]
    new_sample_size = len(new_times)

    # Initialize downsampled DataFrame
    downsampled_data = {"time": new_times}

    # Downsample each data column (excluding time)
    data_columns = [col for col in wec_df.columns if col != "time"]

    for col in data_columns:
        downsampled_values = np.zeros(new_sample_size)

        for i in range(new_sample_size):
            if i == 0:
                # First window: from start to first sample point
                start_idx = 0
                end_idx = min(t_sample, len(wec_df))
            else:
                # Subsequent windows: fixed-width windows
                start_idx = (i - 1) * t_sample
                end_idx = min(i * t_sample, len(wec_df))

            if start_idx < len(wec_df) and end_idx > start_idx:
                downsampled_values[i] = wec_df[col].iloc[start_idx:end_idx].mean()
            else:
                downsampled_values[i] = 0.0  # Handle edge cases

        downsampled_data[col] = downsampled_values

    return pd.DataFrame(downsampled_data)

power_at_snapshot(timestamp)

Calculate total farm power output at a specific simulation time.

Aggregates active power output from all WEC devices in the farm at the specified timestamp. This method provides the primary interface for power system integration, enabling time-varying renewable generation modeling in grid simulations.

Parameters:

Name Type Description Default
timestamp Timestamp

Simulation time to query for power output. Must exist in the device DataFrame time index. Typically corresponds to grid simulation snapshots at 5-minute intervals.

required

Returns:

Name Type Description
float float

Total active power output from all farm devices in per-unit on the farm's sbase. Sum of individual device outputs at the specified time. Returns 0.0 if no valid data available at timestamp.

Raises:

Type Description
KeyError

If timestamp not found in device data index.

AttributeError

If device DataFrame not properly initialized.

Example

Get power at specific simulation time

timestamp = pd.Timestamp("2023-01-01 12:00:00") power_pu = farm.power_at_snapshot(timestamp) print(f"Farm output at noon: {power_pu:.4f} pu") Farm output at noon: 0.1575 pu

Time series power extraction

time_series = [] for snapshot in time_manager.snapshots: ... power_pu = farm.power_at_snapshot(snapshot) ... time_series.append(power_pu)

import matplotlib.pyplot as plt plt.plot(time_manager.snapshots, time_series) plt.ylabel("Farm Power Output [pu]")

Power Aggregation
  • Linear summation: Total = Σ(device_power[i] at timestamp)
  • Homogeneous devices: All devices have identical power profiles
  • Realistic scaling: Based on actual WEC device physics
  • Wave correlation: Devices respond to same ocean conditions
Data Requirements
  • Valid timestamp: Must exist in device DataFrame index
  • Initialized devices: All WECDevice objects must be properly created
  • Power column: Device data must contain "p" column for active power
  • Time alignment: Timestamp must match grid simulation schedule
Error Handling
  • Missing data warning: Prints warning for devices with no data
  • Graceful degradation: Continues calculation with available devices
  • Zero fallback: Returns 0.0 if no devices have valid data
  • Timestamp validation: Checks for existence in device index
Performance Considerations
  • O(n) complexity: Scales linearly with number of devices
  • DataFrame lookup: Efficient pandas indexing for time queries
  • Memory efficiency: No data copying, direct access to device data
  • Repeated calls: Suitable for time-series iteration
Grid Integration Usage
  • PSS®E integration: Provides generator output at each time step
  • PyPSA integration: Supplies renewable generation time series
  • Load flow studies: Time-varying injection for stability analysis
  • Economic dispatch: Variable renewable generation modeling
Wave Energy Characteristics
  • Intermittent output: Power varies with wave conditions
  • Predictable patterns: Follows ocean wave statistics
  • Seasonal variation: Higher output in winter storm seasons
  • Capacity factor: Typically 20-40% for ocean wave resources
Notes
  • Output is in per-unit on the farm's sbase; multiply by sbase for MW
  • Power output includes WEC device efficiency and control effects
  • All devices share identical profiles (same wave field assumption)
  • Negative power values possible during reactive conditions
  • Zero output during calm conditions or device maintenance
  • Farm total limited by grid connection capacity
See Also

WECDevice.dataframe: Individual device power time series Engine.simulate: Uses this method for grid integration WECGridPlotter.plot_wec_analysis: Visualizes farm power output

Source code in src/wecgrid/wec/farm.py
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
def power_at_snapshot(self, timestamp: pd.Timestamp) -> float:
    """Calculate total farm power output at a specific simulation time.

    Aggregates active power output from all WEC devices in the farm at the
    specified timestamp. This method provides the primary interface for
    power system integration, enabling time-varying renewable generation
    modeling in grid simulations.

    Args:
        timestamp (pd.Timestamp): Simulation time to query for power output.
            Must exist in the device DataFrame time index. Typically corresponds
            to grid simulation snapshots at 5-minute intervals.

    Returns:
        float: Total active power output from all farm devices in per-unit on
            the farm's ``sbase``. Sum of individual device outputs at the
            specified time. Returns 0.0 if no valid data available at
            timestamp.

    Raises:
        KeyError: If timestamp not found in device data index.
        AttributeError: If device DataFrame not properly initialized.

    Example:
        >>> # Get power at specific simulation time
        >>> timestamp = pd.Timestamp("2023-01-01 12:00:00")
        >>> power_pu = farm.power_at_snapshot(timestamp)
        >>> print(f"Farm output at noon: {power_pu:.4f} pu")
        Farm output at noon: 0.1575 pu

        >>> # Time series power extraction
        >>> time_series = []
        >>> for snapshot in time_manager.snapshots:
        ...     power_pu = farm.power_at_snapshot(snapshot)
        ...     time_series.append(power_pu)
        >>>
        >>> import matplotlib.pyplot as plt
        >>> plt.plot(time_manager.snapshots, time_series)
        >>> plt.ylabel("Farm Power Output [pu]")

    Power Aggregation:
        - **Linear summation**: Total = Σ(device_power[i] at timestamp)
        - **Homogeneous devices**: All devices have identical power profiles
        - **Realistic scaling**: Based on actual WEC device physics
        - **Wave correlation**: Devices respond to same ocean conditions

    Data Requirements:
        - **Valid timestamp**: Must exist in device DataFrame index
        - **Initialized devices**: All WECDevice objects must be properly created
        - **Power column**: Device data must contain "p" column for active power
        - **Time alignment**: Timestamp must match grid simulation schedule

    Error Handling:
        - **Missing data warning**: Prints warning for devices with no data
        - **Graceful degradation**: Continues calculation with available devices
        - **Zero fallback**: Returns 0.0 if no devices have valid data
        - **Timestamp validation**: Checks for existence in device index

    Performance Considerations:
        - **O(n) complexity**: Scales linearly with number of devices
        - **DataFrame lookup**: Efficient pandas indexing for time queries
        - **Memory efficiency**: No data copying, direct access to device data
        - **Repeated calls**: Suitable for time-series iteration

    Grid Integration Usage:
        - **PSS®E integration**: Provides generator output at each time step
        - **PyPSA integration**: Supplies renewable generation time series
        - **Load flow studies**: Time-varying injection for stability analysis
        - **Economic dispatch**: Variable renewable generation modeling

    Wave Energy Characteristics:
        - **Intermittent output**: Power varies with wave conditions
        - **Predictable patterns**: Follows ocean wave statistics
        - **Seasonal variation**: Higher output in winter storm seasons
        - **Capacity factor**: Typically 20-40% for ocean wave resources

    Notes:
        - Output is in per-unit on the farm's ``sbase``; multiply by
          ``sbase`` for MW
        - Power output includes WEC device efficiency and control effects
        - All devices share identical profiles (same wave field assumption)
        - Negative power values possible during reactive conditions
        - Zero output during calm conditions or device maintenance
        - Farm total limited by grid connection capacity

    See Also:
        WECDevice.dataframe: Individual device power time series
        Engine.simulate: Uses this method for grid integration
        WECGridPlotter.plot_wec_analysis: Visualizes farm power output
    """
    total_power = 0.0
    for device in self.wec_devices:
        if (
            device.dataframe is not None
            and not device.dataframe.empty
            and timestamp in device.dataframe.index
        ):
            power = device.dataframe.at[timestamp, "p"]
            total_power += power
        else:
            print(f"[WARNING] Missing data for {device.name} at {timestamp}")
    return total_power

WECSimRunner

Interface for running WEC-Sim device-level simulations via MATLAB engine.

Simplified runner that manages MATLAB engine, executes WEC-Sim models from their native directories, and stores results in WEC-Grid database.

Attributes:

Name Type Description
wec_sim_path str

Path to WEC-Sim MATLAB installation.

database WECGridDB

Database interface for simulation data storage.

matlab_engine MatlabEngine

Active MATLAB engine.

Source code in src/wecgrid/modelers/wec_sim/runner.py
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
class WECSimRunner:
    """Interface for running WEC-Sim device-level simulations via MATLAB engine.

    Simplified runner that manages MATLAB engine, executes WEC-Sim models from
    their native directories, and stores results in WEC-Grid database.

    Attributes:
        wec_sim_path (str, optional): Path to WEC-Sim MATLAB installation.
        database (WECGridDB): Database interface for simulation data storage.
        matlab_engine (matlab.engine.MatlabEngine, optional): Active MATLAB engine.
    """

    def __init__(self, database: WECGridDB):
        """Initialize a WEC-Sim runner tied to a database connection.

        Args:
            database (WECGridDB): Interface used to store and retrieve
                simulation data.

        Attributes:
            wec_sim_path (Optional[str]): Location of the WEC-Sim MATLAB
                installation. Defaults to ``None`` and may be populated from
                configuration.
            matlab_engine (Optional[matlab.engine.MatlabEngine]): Handle to the
                MATLAB engine. ``None`` until :meth:`start_matlab` is called.

        Side Effects:
            Loads ``wecsim_config.json`` to populate ``wec_sim_path`` if a
            configuration exists.
        """
        self.wec_sim_path: Optional[str] = None
        self.database: WECGridDB = database
        self.matlab_engine = None

    def _load_config(self) -> None:
        """Load WEC-Sim configuration from JSON file."""
        try:
            if os.path.exists(_CONFIG_FILE):
                with open(_CONFIG_FILE, "r") as f:
                    config = json.load(f)
                    self.wec_sim_path = config.get("wec_sim_path")
        except Exception as e:
            print(f"Warning: Could not load WEC-Sim config: {e}")

    def _save_config(self) -> None:
        """Save WEC-Sim configuration to JSON file."""
        try:
            config = {"wec_sim_path": self.wec_sim_path}
            with open(_CONFIG_FILE, "w") as f:
                json.dump(config, f, indent=2)
            print(f"Saved WEC-Sim configuration to: {_CONFIG_FILE}")
        except Exception as e:
            print(f"Warning: Could not save WEC-Sim config: {e}")

    def set_wec_sim_path(self, path: str) -> None:
        """Configure the WEC-Sim MATLAB framework installation path.

        Args:
            path (str): Filesystem location of the WEC-Sim MATLAB installation.

        Raises:
            FileNotFoundError: If the supplied ``path`` does not exist.
        """
        if not os.path.exists(path):
            raise FileNotFoundError(f"WEC-SIM path does not exist: {path}")
        self.wec_sim_path = path
        self._save_config()

    def get_wec_sim_path(self) -> Optional[str]:
        """Get the currently configured WEC-Sim path.

        Returns:
            Optional[str]: Absolute path to the WEC-Sim installation or ``None``
            if no path has been configured.
        """
        return self.wec_sim_path

    def show_config(self) -> None:
        """Display current WEC-Sim configuration.

        Prints the currently configured WEC-Sim path along with the location of
        the configuration file used to persist this setting.
        """
        print(f"WEC-Sim Configuration:")
        print(f"  Path: {self.wec_sim_path or 'Not configured'}")
        print(f"  Config file: {_CONFIG_FILE}")
        print(f"  Config exists: {os.path.exists(_CONFIG_FILE)}")

    def start_matlab(self) -> bool:
        """Initialize MATLAB engine and configure WEC-Sim framework paths.

        Returns:
            bool: ``True`` if the MATLAB engine was started, ``False`` if the
            engine was already running or the MATLAB Python API is unavailable.
        """
        self._load_config()
        if self.wec_sim_path is None:
            print("\n" + "=" * 60)
            print("WEC-Sim Path not set")
            print("=" * 60)
            print(
                "Please set the WEC-Sim path here or using the wecsim.set_wec_sim_path() method."
            )
            path = input("Enter the WEC-Sim path: ")
            self.set_wec_sim_path(path)
        try:
            import matlab.engine
        except ImportError:
            print("MATLAB python API not installed. ")
            print(
                "https://www.mathworks.com/help/matlab/matlab_external/install-the-matlab-engine-for-python.html"
            )
            return False
        if self.matlab_engine is None:
            print(f"Starting MATLAB Engine... ", end="")
            self.matlab_engine = matlab.engine.start_matlab()
            print("MATLAB engine started.")

            if self.wec_sim_path is None:
                raise ValueError(
                    "WEC-SIM path is not configured. Please set it using set_wec_sim_path()"
                )

            if not os.path.exists(self.wec_sim_path):
                raise FileNotFoundError(
                    f"WEC-SIM path does not exist: {self.wec_sim_path}"
                )
            print(f"Adding WEC-SIM to path... ", end="")
            matlab_path = self.matlab_engine.genpath(str(self.wec_sim_path), nargout=1)
            self.matlab_engine.addpath(matlab_path, nargout=0)
            print("WEC-SIM path added.")

            self.out = io.StringIO()
            self.err = io.StringIO()
            return True
        else:
            print("MATLAB engine is already running.")
            return False

    def stop_matlab(self) -> bool:
        """Shutdown the MATLAB engine and free system resources.

        Returns:
            bool: ``True`` if the engine was stopped, ``False`` if no engine was
            running.
        """
        if self.matlab_engine is not None:
            self.matlab_engine.quit()
            self.matlab_engine = None
            print("MATLAB engine stopped.")
            self.out = None
            self.err = None
            return True
        print("MATLAB engine is not running.")
        return False

    def sim_results(self, df_power, model, wec_sim_id):
        """Generate visualization plots for WEC-Sim simulation results.

        Args:
            df_power (pd.DataFrame): Power and optional wave elevation time
                series produced by WEC-Sim.
            model (str): Name of the WEC-Sim model used for the simulation.
            wec_sim_id (int): Database identifier for the WEC-Sim run.
        """
        if df_power.empty:
            print("No power data available for visualization")
            return

        # Plot
        fig, ax1 = plt.subplots(figsize=(10, 5))

        # Secondary y-axis: Wave elevation (m) — drawn first for background
        ax2 = ax1.twinx()
        ax2.set_ylabel("Wave Elevation (m)")
        if "eta" in df_power.columns:
            ax2.plot(
                df_power["time"],
                df_power["eta"],
                color="tab:blue",
                alpha=0.3,
                linewidth=1,
                label="Wave Elevation",
            )

        # Primary y-axis: Active power W
        ax1.set_xlabel("Time (s)")
        ax1.set_ylabel("Active Power W")
        ax1.plot(
            df_power["time"],
            df_power["p"],
            color="tab:red",
            label="Power Output",
            linewidth=1.5,
        )

        # Title + layout
        fig.suptitle(f"WEC-SIM Output — Model: {model}, WEC Sim ID: {wec_sim_id}")
        fig.tight_layout()

        # Combine legends
        lines_1, labels_1 = ax1.get_legend_handles_labels()
        lines_2, labels_2 = ax2.get_legend_handles_labels()
        ax1.legend(lines_1 + lines_2, labels_1 + labels_2, loc="upper right")

        plt.show()

    def __call__(
        self,
        model_path: str,
        sim_length: int = 3600 * 24,  # 24 hours
        delta_time: float = 0.1,
        spectrum_type: str = "PM",
        wave_class: str = "irregular",
        wave_height: float = 2.5,
        wave_period: float = 8.0,
        wave_seed: int = random.randint(1, 100),
    ) -> Optional[int]:
        """Execute a complete WEC-Sim device simulation with specified parameters.

        Args:
            model_path (str): Path to WEC model directory containing simulation files.
            sim_length (int, optional): Simulation duration in seconds. Defaults to 86400 (24 hours).
            delta_time (float, optional): Simulation time step in seconds. Defaults to 0.1.
            spectrum_type (str, optional): Wave spectrum type. Defaults to 'PM'.
            wave_class (str, optional): Wave type classification. Defaults to 'irregular'.
            wave_height (float, optional): Significant wave height in meters. Defaults to 2.5.
            wave_period (float, optional): Peak wave period in seconds. Defaults to 8.0.
            wave_seed (int, optional): Random seed for wave generation. Defaults to random 1-100.

        Returns:
            int: wec_sim_id from database if successful, None if failed.
        """
        print(
            r"""

            ⠀ WEC-SIM⠀⠀⠀⠀     ⣠⣴⣶⠾⠿⠿⠯⣷⣄⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
            ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⣼⣾⠛⠁⠀⠀⠀⠀⠀⠀⠈⢻⣦⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
            ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣠⣾⠿⠁⠀⠀⠀⢀⣤⣾⣟⣛⣛⣶⣬⣿⣆⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
            ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⣾⠟⠃⠀⠀⠀⠀⠀⣾⣿⠟⠉⠉⠉⠉⠛⠿⠟⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
            ⠀⠀⠀⠀⠀⠀⠀⠀⢀⣴⡟⠋⠀⠀⠀⠀⠀⠀⠀⣿⡏⣤⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
            ⠀⠀⠀⠀⠀⠀⠀⣠⡿⠛⠀⠀⠀⠀⠀⠀⠀⠀⠀⠙⣷⡍⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⣀⣀⣤⣤⣤⣀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
            ⠀⠀⠀⠀⠀⣠⣼⡏⠀⠀           ⠈⠙⠷⣤⣤⣠⣤⣤⡤⡶⣶⢿⠟⠹⠿⠄⣿⣿⠏⠀⣀⣤⡦⠀⠀⠀⠀⣀⡄
            ⢀⣄⣠⣶⣿⠏⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠈⠉⠓⠚⠋⠉⠀⠀⠀⠀⠀⠀⠈⠛⡛⡻⠿⠿⠙⠓⢒⣺⡿⠋⠁
            ⠉⠉⠉⠛⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠉⠉⠁⠀
            """
        )

        try:
            # Validate model path
            if not os.path.exists(model_path):
                raise FileNotFoundError(f"WEC model path '{model_path}' does not exist")

            model_name = os.path.basename(model_path)

            if self.start_matlab():
                print("Starting WEC-SIM simulation...")
                print(
                    f"\t Model: {model_name}\n"
                    f"\t Model Path: {model_path}\n"
                    f"\t Simulation Length: {sim_length} seconds\n"
                    f"\t Time Step: {delta_time} seconds\n"
                    f"\t Wave class: {wave_class}\n"
                    f"\t Wave Height: {wave_height} m\n"
                    f"\t Wave Period: {wave_period} s\n"
                )

                # Change to model directory - this is the key change from the old version
                self.matlab_engine.cd(str(model_path))

                # Set simulation parameters in MATLAB workspace
                self.matlab_engine.workspace["simLength"] = sim_length
                self.matlab_engine.workspace["dt"] = delta_time
                self.matlab_engine.workspace["spectrumType"] = spectrum_type
                self.matlab_engine.workspace["waveClassType"] = wave_class
                self.matlab_engine.workspace["waveHeight"] = wave_height
                self.matlab_engine.workspace["wavePeriod"] = wave_period
                self.matlab_engine.workspace["waveSeed"] = int(wave_seed)
                self.matlab_engine.workspace["DB_PATH"] = self.database.db_path
                out = io.StringIO()
                err = io.StringIO()

                # Run the WEC-SIM function from the model directory
                self.matlab_engine.eval(
                    "[m2g_out] = w2gSim(simLength,dt,spectrumType,waveClassType,waveHeight,wavePeriod,waveSeed);",
                    nargout=0,
                    stdout=out,
                    stderr=err,
                )
                print(
                    f"simulation complete... writing to database at \n\t{self.database.db_path}"
                )

                self.matlab_engine.eval("formatter", nargout=0, stdout=out, stderr=err)

                # Get the wec_sim_id that was created by the database
                wec_sim_id = self.matlab_engine.workspace["wec_sim_id_result"]
                wec_sim_id = int(wec_sim_id)  # Convert from MATLAB double to Python int

                print(
                    f"WEC-SIM complete: model = {model_name}, wec_sim_id = {wec_sim_id}, duration = {sim_length}s"
                )

                # Query power results for visualization
                power_query = """
                    SELECT time_sec as time, p_w as p, wave_elevation_m as eta 
                    FROM wec_power_results 
                    WHERE wec_sim_id = ? 
                    ORDER BY time_sec
                """
                df_power = self.database.query(
                    power_query, params=(wec_sim_id,), return_type="df"
                )
                print("MATLAB Output:")
                print("=" * 10)
                print(out.getvalue())
                print("=" * 10)
                self.stop_matlab()

                if not df_power.empty:
                    self.sim_results(df_power, model_name, wec_sim_id)

                return wec_sim_id

            print("Failed to start MATLAB engine.")
            return None

        except Exception as e:
            print(f"[WEC-SIM ERROR] model_path={model_path}{e}")
            print("=" * 10)
            print("MATLAB Output:")
            print(out.getvalue())
            print("MATLAB Errors:")
            print(err.getvalue())
            print("=" * 10)

            self.stop_matlab()
            return None

get_wec_sim_path()

Get the currently configured WEC-Sim path.

Returns:

Type Description
Optional[str]

Optional[str]: Absolute path to the WEC-Sim installation or None

Optional[str]

if no path has been configured.

Source code in src/wecgrid/modelers/wec_sim/runner.py
 94
 95
 96
 97
 98
 99
100
101
def get_wec_sim_path(self) -> Optional[str]:
    """Get the currently configured WEC-Sim path.

    Returns:
        Optional[str]: Absolute path to the WEC-Sim installation or ``None``
        if no path has been configured.
    """
    return self.wec_sim_path

set_wec_sim_path(path)

Configure the WEC-Sim MATLAB framework installation path.

Parameters:

Name Type Description Default
path str

Filesystem location of the WEC-Sim MATLAB installation.

required

Raises:

Type Description
FileNotFoundError

If the supplied path does not exist.

Source code in src/wecgrid/modelers/wec_sim/runner.py
80
81
82
83
84
85
86
87
88
89
90
91
92
def set_wec_sim_path(self, path: str) -> None:
    """Configure the WEC-Sim MATLAB framework installation path.

    Args:
        path (str): Filesystem location of the WEC-Sim MATLAB installation.

    Raises:
        FileNotFoundError: If the supplied ``path`` does not exist.
    """
    if not os.path.exists(path):
        raise FileNotFoundError(f"WEC-SIM path does not exist: {path}")
    self.wec_sim_path = path
    self._save_config()

show_config()

Display current WEC-Sim configuration.

Prints the currently configured WEC-Sim path along with the location of the configuration file used to persist this setting.

Source code in src/wecgrid/modelers/wec_sim/runner.py
103
104
105
106
107
108
109
110
111
112
def show_config(self) -> None:
    """Display current WEC-Sim configuration.

    Prints the currently configured WEC-Sim path along with the location of
    the configuration file used to persist this setting.
    """
    print(f"WEC-Sim Configuration:")
    print(f"  Path: {self.wec_sim_path or 'Not configured'}")
    print(f"  Config file: {_CONFIG_FILE}")
    print(f"  Config exists: {os.path.exists(_CONFIG_FILE)}")

sim_results(df_power, model, wec_sim_id)

Generate visualization plots for WEC-Sim simulation results.

Parameters:

Name Type Description Default
df_power DataFrame

Power and optional wave elevation time series produced by WEC-Sim.

required
model str

Name of the WEC-Sim model used for the simulation.

required
wec_sim_id int

Database identifier for the WEC-Sim run.

required
Source code in src/wecgrid/modelers/wec_sim/runner.py
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
def sim_results(self, df_power, model, wec_sim_id):
    """Generate visualization plots for WEC-Sim simulation results.

    Args:
        df_power (pd.DataFrame): Power and optional wave elevation time
            series produced by WEC-Sim.
        model (str): Name of the WEC-Sim model used for the simulation.
        wec_sim_id (int): Database identifier for the WEC-Sim run.
    """
    if df_power.empty:
        print("No power data available for visualization")
        return

    # Plot
    fig, ax1 = plt.subplots(figsize=(10, 5))

    # Secondary y-axis: Wave elevation (m) — drawn first for background
    ax2 = ax1.twinx()
    ax2.set_ylabel("Wave Elevation (m)")
    if "eta" in df_power.columns:
        ax2.plot(
            df_power["time"],
            df_power["eta"],
            color="tab:blue",
            alpha=0.3,
            linewidth=1,
            label="Wave Elevation",
        )

    # Primary y-axis: Active power W
    ax1.set_xlabel("Time (s)")
    ax1.set_ylabel("Active Power W")
    ax1.plot(
        df_power["time"],
        df_power["p"],
        color="tab:red",
        label="Power Output",
        linewidth=1.5,
    )

    # Title + layout
    fig.suptitle(f"WEC-SIM Output — Model: {model}, WEC Sim ID: {wec_sim_id}")
    fig.tight_layout()

    # Combine legends
    lines_1, labels_1 = ax1.get_legend_handles_labels()
    lines_2, labels_2 = ax2.get_legend_handles_labels()
    ax1.legend(lines_1 + lines_2, labels_1 + labels_2, loc="upper right")

    plt.show()

start_matlab()

Initialize MATLAB engine and configure WEC-Sim framework paths.

Returns:

Name Type Description
bool bool

True if the MATLAB engine was started, False if the

bool

engine was already running or the MATLAB Python API is unavailable.

Source code in src/wecgrid/modelers/wec_sim/runner.py
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
def start_matlab(self) -> bool:
    """Initialize MATLAB engine and configure WEC-Sim framework paths.

    Returns:
        bool: ``True`` if the MATLAB engine was started, ``False`` if the
        engine was already running or the MATLAB Python API is unavailable.
    """
    self._load_config()
    if self.wec_sim_path is None:
        print("\n" + "=" * 60)
        print("WEC-Sim Path not set")
        print("=" * 60)
        print(
            "Please set the WEC-Sim path here or using the wecsim.set_wec_sim_path() method."
        )
        path = input("Enter the WEC-Sim path: ")
        self.set_wec_sim_path(path)
    try:
        import matlab.engine
    except ImportError:
        print("MATLAB python API not installed. ")
        print(
            "https://www.mathworks.com/help/matlab/matlab_external/install-the-matlab-engine-for-python.html"
        )
        return False
    if self.matlab_engine is None:
        print(f"Starting MATLAB Engine... ", end="")
        self.matlab_engine = matlab.engine.start_matlab()
        print("MATLAB engine started.")

        if self.wec_sim_path is None:
            raise ValueError(
                "WEC-SIM path is not configured. Please set it using set_wec_sim_path()"
            )

        if not os.path.exists(self.wec_sim_path):
            raise FileNotFoundError(
                f"WEC-SIM path does not exist: {self.wec_sim_path}"
            )
        print(f"Adding WEC-SIM to path... ", end="")
        matlab_path = self.matlab_engine.genpath(str(self.wec_sim_path), nargout=1)
        self.matlab_engine.addpath(matlab_path, nargout=0)
        print("WEC-SIM path added.")

        self.out = io.StringIO()
        self.err = io.StringIO()
        return True
    else:
        print("MATLAB engine is already running.")
        return False

stop_matlab()

Shutdown the MATLAB engine and free system resources.

Returns:

Name Type Description
bool bool

True if the engine was stopped, False if no engine was

bool

running.

Source code in src/wecgrid/modelers/wec_sim/runner.py
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
def stop_matlab(self) -> bool:
    """Shutdown the MATLAB engine and free system resources.

    Returns:
        bool: ``True`` if the engine was stopped, ``False`` if no engine was
        running.
    """
    if self.matlab_engine is not None:
        self.matlab_engine.quit()
        self.matlab_engine = None
        print("MATLAB engine stopped.")
        self.out = None
        self.err = None
        return True
    print("MATLAB engine is not running.")
    return False