phased-array-systems 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. phased_array_systems/__about__.py +4 -0
  2. phased_array_systems/__init__.py +10 -0
  3. phased_array_systems/architecture/__init__.py +15 -0
  4. phased_array_systems/architecture/config.py +152 -0
  5. phased_array_systems/cli.py +25 -0
  6. phased_array_systems/constants.py +55 -0
  7. phased_array_systems/evaluate.py +136 -0
  8. phased_array_systems/io/__init__.py +13 -0
  9. phased_array_systems/io/config_loader.py +86 -0
  10. phased_array_systems/io/exporters.py +171 -0
  11. phased_array_systems/io/schema.py +145 -0
  12. phased_array_systems/models/__init__.py +5 -0
  13. phased_array_systems/models/antenna/__init__.py +15 -0
  14. phased_array_systems/models/antenna/adapter.py +190 -0
  15. phased_array_systems/models/antenna/metrics.py +166 -0
  16. phased_array_systems/models/base.py +30 -0
  17. phased_array_systems/models/comms/__init__.py +9 -0
  18. phased_array_systems/models/comms/link_budget.py +171 -0
  19. phased_array_systems/models/comms/propagation.py +84 -0
  20. phased_array_systems/models/swapc/__init__.py +9 -0
  21. phased_array_systems/models/swapc/cost.py +98 -0
  22. phased_array_systems/models/swapc/power.py +102 -0
  23. phased_array_systems/requirements/__init__.py +15 -0
  24. phased_array_systems/requirements/core.py +244 -0
  25. phased_array_systems/scenarios/__init__.py +11 -0
  26. phased_array_systems/scenarios/base.py +30 -0
  27. phased_array_systems/scenarios/comms.py +56 -0
  28. phased_array_systems/scenarios/radar.py +42 -0
  29. phased_array_systems/trades/__init__.py +16 -0
  30. phased_array_systems/trades/design_space.py +241 -0
  31. phased_array_systems/trades/doe.py +146 -0
  32. phased_array_systems/trades/pareto.py +266 -0
  33. phased_array_systems/trades/runner.py +245 -0
  34. phased_array_systems/types.py +54 -0
  35. phased_array_systems/utils/__init__.py +8 -0
  36. phased_array_systems/utils/hashing.py +70 -0
  37. phased_array_systems/viz/__init__.py +9 -0
  38. phased_array_systems/viz/plots.py +324 -0
  39. phased_array_systems-0.1.0.dist-info/METADATA +174 -0
  40. phased_array_systems-0.1.0.dist-info/RECORD +43 -0
  41. phased_array_systems-0.1.0.dist-info/WHEEL +4 -0
  42. phased_array_systems-0.1.0.dist-info/entry_points.txt +2 -0
  43. phased_array_systems-0.1.0.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,145 @@
1
+ """Configuration schema definitions using Pydantic."""
2
+
3
+ from typing import Any, Literal
4
+
5
+ from pydantic import BaseModel, Field
6
+
7
+ from phased_array_systems.architecture import (
8
+ Architecture,
9
+ ArrayConfig,
10
+ CostConfig,
11
+ RFChainConfig,
12
+ )
13
+ from phased_array_systems.requirements import Requirement, RequirementSet
14
+ from phased_array_systems.scenarios import CommsLinkScenario, RadarDetectionScenario
15
+ from phased_array_systems.types import ComparisonOp, Severity
16
+
17
+
18
+ class RequirementConfig(BaseModel):
19
+ """Configuration for a single requirement."""
20
+
21
+ id: str
22
+ name: str
23
+ metric_key: str
24
+ op: ComparisonOp
25
+ value: float
26
+ units: str | None = None
27
+ severity: Severity = "must"
28
+
29
+ def to_requirement(self) -> Requirement:
30
+ """Convert to Requirement object."""
31
+ return Requirement(
32
+ id=self.id,
33
+ name=self.name,
34
+ metric_key=self.metric_key,
35
+ op=self.op,
36
+ value=self.value,
37
+ units=self.units,
38
+ severity=self.severity,
39
+ )
40
+
41
+
42
+ class DesignVariableConfig(BaseModel):
43
+ """Configuration for a design variable in DOE studies."""
44
+
45
+ name: str = Field(description="Variable name (e.g., 'array.nx')")
46
+ type: Literal["int", "float", "categorical"] = "float"
47
+ low: float | None = Field(default=None, description="Lower bound")
48
+ high: float | None = Field(default=None, description="Upper bound")
49
+ values: list[Any] | None = Field(default=None, description="Categorical values")
50
+
51
+
52
+ class DOEConfig(BaseModel):
53
+ """Configuration for Design of Experiments."""
54
+
55
+ method: Literal["grid", "random", "lhs"] = "lhs"
56
+ n_samples: int = Field(default=100, ge=1, description="Number of samples")
57
+ seed: int | None = Field(default=None, description="Random seed")
58
+ variables: list[DesignVariableConfig] = Field(default_factory=list)
59
+
60
+
61
+ class StudyConfig(BaseModel):
62
+ """Top-level configuration for a study.
63
+
64
+ Supports both single-case evaluation and DOE trade studies.
65
+
66
+ Example YAML:
67
+ ```yaml
68
+ name: "Comms Array Study"
69
+ architecture:
70
+ array:
71
+ nx: 8
72
+ ny: 8
73
+ dx_lambda: 0.5
74
+ rf:
75
+ tx_power_w_per_elem: 1.0
76
+ scenario:
77
+ type: comms
78
+ freq_hz: 10e9
79
+ bandwidth_hz: 10e6
80
+ range_m: 100e3
81
+ required_snr_db: 10.0
82
+ requirements:
83
+ - id: REQ-001
84
+ name: Minimum EIRP
85
+ metric_key: eirp_dbw
86
+ op: ">="
87
+ value: 40.0
88
+ ```
89
+ """
90
+
91
+ name: str = Field(default="Unnamed Study", description="Study name")
92
+ version: str = Field(default="1.0", description="Config version")
93
+
94
+ # Architecture configuration
95
+ architecture: Architecture | None = None
96
+ array: ArrayConfig | None = Field(default=None, description="Array config (shorthand)")
97
+ rf: RFChainConfig | None = Field(default=None, description="RF config (shorthand)")
98
+ cost: CostConfig | None = Field(default=None, description="Cost config (shorthand)")
99
+
100
+ # Scenario configuration
101
+ scenario: dict[str, Any] | None = Field(default=None, description="Scenario definition")
102
+
103
+ # Requirements
104
+ requirements: list[RequirementConfig] = Field(default_factory=list)
105
+
106
+ # DOE configuration (optional, for trade studies)
107
+ doe: DOEConfig | None = None
108
+
109
+ # Output configuration
110
+ output_dir: str = Field(default="./results", description="Output directory")
111
+ output_format: Literal["parquet", "csv", "json"] = "parquet"
112
+
113
+ def get_architecture(self) -> Architecture:
114
+ """Get the Architecture object, building from shorthand if needed."""
115
+ if self.architecture is not None:
116
+ return self.architecture
117
+
118
+ # Build from shorthand configs
119
+ array = self.array or ArrayConfig(nx=8, ny=8)
120
+ rf = self.rf or RFChainConfig(tx_power_w_per_elem=1.0)
121
+ cost = self.cost or CostConfig()
122
+
123
+ return Architecture(array=array, rf=rf, cost=cost, name=self.name)
124
+
125
+ def get_scenario(self) -> CommsLinkScenario | RadarDetectionScenario | None:
126
+ """Get the Scenario object from config."""
127
+ if self.scenario is None:
128
+ return None
129
+
130
+ scenario_dict = self.scenario.copy()
131
+ scenario_type = scenario_dict.pop("type", "comms")
132
+
133
+ if scenario_type == "comms":
134
+ return CommsLinkScenario(**scenario_dict)
135
+ elif scenario_type == "radar":
136
+ return RadarDetectionScenario(**scenario_dict)
137
+ else:
138
+ raise ValueError(f"Unknown scenario type: {scenario_type}")
139
+
140
+ def get_requirement_set(self) -> RequirementSet:
141
+ """Get RequirementSet from config."""
142
+ req_set = RequirementSet(name=f"{self.name} Requirements")
143
+ for req_config in self.requirements:
144
+ req_set.add(req_config.to_requirement())
145
+ return req_set
@@ -0,0 +1,5 @@
1
+ """Model blocks for system analysis."""
2
+
3
+ from phased_array_systems.models.base import ModelBlock
4
+
5
+ __all__ = ["ModelBlock"]
@@ -0,0 +1,15 @@
1
+ """Antenna modeling adapter wrapping phased-array-modeling."""
2
+
3
+ from phased_array_systems.models.antenna.adapter import PhasedArrayAdapter
4
+ from phased_array_systems.models.antenna.metrics import (
5
+ compute_beamwidth,
6
+ compute_scan_loss,
7
+ compute_sidelobe_level,
8
+ )
9
+
10
+ __all__ = [
11
+ "PhasedArrayAdapter",
12
+ "compute_beamwidth",
13
+ "compute_scan_loss",
14
+ "compute_sidelobe_level",
15
+ ]
@@ -0,0 +1,190 @@
1
+ """Adapter wrapping phased-array-modeling for consistent metric extraction."""
2
+
3
+ from typing import Any
4
+
5
+ import numpy as np
6
+
7
+ from phased_array_systems.architecture import Architecture
8
+ from phased_array_systems.models.antenna.metrics import (
9
+ compute_beamwidth,
10
+ compute_directivity_rectangular,
11
+ compute_scan_loss,
12
+ compute_sidelobe_level,
13
+ )
14
+ from phased_array_systems.types import MetricsDict, Scenario
15
+
16
+ # Try to import phased-array-modeling, fall back to stub if not available
17
+ try:
18
+ from phased_array_modeling import (
19
+ RectangularArray,
20
+ compute_array_factor,
21
+ )
22
+
23
+ HAS_PAM = True
24
+ except ImportError:
25
+ HAS_PAM = False
26
+
27
+
28
+ class PhasedArrayAdapter:
29
+ """Adapter for phased-array-modeling library.
30
+
31
+ Provides a consistent interface for computing antenna pattern metrics
32
+ using the phased-array-modeling library, with fallback to analytical
33
+ approximations when the library is not available.
34
+
35
+ Attributes:
36
+ name: Model block name for identification
37
+ use_analytical_fallback: If True, use analytical approximations
38
+ when phased-array-modeling is not available
39
+ """
40
+
41
+ name: str = "antenna"
42
+
43
+ def __init__(self, use_analytical_fallback: bool = True):
44
+ """Initialize the adapter.
45
+
46
+ Args:
47
+ use_analytical_fallback: Use analytical methods if PAM unavailable
48
+ """
49
+ self.use_analytical_fallback = use_analytical_fallback
50
+
51
+ if not HAS_PAM and not use_analytical_fallback:
52
+ raise ImportError(
53
+ "phased-array-modeling not installed. Install with: "
54
+ "pip install phased-array-modeling"
55
+ )
56
+
57
+ def evaluate(
58
+ self, arch: Architecture, scenario: Scenario, context: dict[str, Any]
59
+ ) -> MetricsDict:
60
+ """Evaluate antenna performance metrics.
61
+
62
+ Args:
63
+ arch: Architecture configuration
64
+ scenario: Scenario with frequency and scan angle info
65
+ context: Additional context (unused)
66
+
67
+ Returns:
68
+ Dictionary with antenna metrics:
69
+ - g_peak_db: Peak array gain (dB)
70
+ - beamwidth_az_deg: Azimuth beamwidth (degrees)
71
+ - beamwidth_el_deg: Elevation beamwidth (degrees)
72
+ - sll_db: Peak sidelobe level (dB, negative)
73
+ - scan_loss_db: Scan loss at operating angle (dB)
74
+ - directivity_db: Array directivity (dB)
75
+ - n_elements: Number of array elements
76
+ """
77
+ # Extract scan angle from scenario if available
78
+ scan_angle_deg = getattr(scenario, "scan_angle_deg", 0.0)
79
+
80
+ if HAS_PAM:
81
+ return self._evaluate_with_pam(arch, scenario, scan_angle_deg)
82
+ else:
83
+ return self._evaluate_analytical(arch, scenario, scan_angle_deg)
84
+
85
+ def _evaluate_with_pam(
86
+ self, arch: Architecture, scenario: Scenario, scan_angle_deg: float
87
+ ) -> MetricsDict:
88
+ """Evaluate using phased-array-modeling library."""
89
+ wavelength_m = scenario.wavelength_m if hasattr(scenario, "wavelength_m") else None
90
+
91
+ if wavelength_m is None:
92
+ from phased_array_systems.constants import C
93
+ wavelength_m = C / scenario.freq_hz
94
+
95
+ # Create array object
96
+ array = RectangularArray(
97
+ nx=arch.array.nx,
98
+ ny=arch.array.ny,
99
+ dx=arch.array.dx_lambda * wavelength_m,
100
+ dy=arch.array.dy_lambda * wavelength_m,
101
+ )
102
+
103
+ # Compute array factor over theta range
104
+ theta_deg = np.linspace(-90, 90, 361)
105
+ theta_rad = np.radians(theta_deg)
106
+
107
+ # Compute AF at phi=0 (azimuth cut)
108
+ af_az = compute_array_factor(
109
+ array,
110
+ theta_rad,
111
+ phi=0,
112
+ wavelength=wavelength_m,
113
+ scan_theta=np.radians(scan_angle_deg),
114
+ scan_phi=0,
115
+ )
116
+ af_az_db = 20 * np.log10(np.abs(af_az) + 1e-12)
117
+ af_az_db = af_az_db - np.max(af_az_db) # Normalize to peak
118
+
119
+ # Compute AF at phi=90 (elevation cut)
120
+ af_el = compute_array_factor(
121
+ array,
122
+ theta_rad,
123
+ phi=np.pi / 2,
124
+ wavelength=wavelength_m,
125
+ scan_theta=np.radians(scan_angle_deg),
126
+ scan_phi=0,
127
+ )
128
+ af_el_db = 20 * np.log10(np.abs(af_el) + 1e-12)
129
+ af_el_db = af_el_db - np.max(af_el_db)
130
+
131
+ # Extract metrics
132
+ beamwidth_az = compute_beamwidth(af_az_db, theta_deg)
133
+ beamwidth_el = compute_beamwidth(af_el_db, theta_deg)
134
+ sll = compute_sidelobe_level(af_az_db, theta_deg)
135
+ scan_loss = compute_scan_loss(scan_angle_deg)
136
+ directivity = compute_directivity_rectangular(
137
+ arch.array.nx, arch.array.ny, arch.array.dx_lambda, arch.array.dy_lambda
138
+ )
139
+ g_peak = directivity - scan_loss # Account for scan loss
140
+
141
+ return {
142
+ "g_peak_db": g_peak,
143
+ "beamwidth_az_deg": beamwidth_az,
144
+ "beamwidth_el_deg": beamwidth_el,
145
+ "sll_db": sll,
146
+ "scan_loss_db": scan_loss,
147
+ "directivity_db": directivity,
148
+ "n_elements": arch.array.n_elements,
149
+ }
150
+
151
+ def _evaluate_analytical(
152
+ self, arch: Architecture, scenario: Scenario, scan_angle_deg: float
153
+ ) -> MetricsDict:
154
+ """Evaluate using analytical approximations.
155
+
156
+ Uses standard phased array formulas when the full simulation
157
+ library is not available.
158
+ """
159
+ # Directivity from aperture size
160
+ directivity_db = compute_directivity_rectangular(
161
+ arch.array.nx, arch.array.ny, arch.array.dx_lambda, arch.array.dy_lambda
162
+ )
163
+
164
+ # Scan loss
165
+ scan_loss = compute_scan_loss(scan_angle_deg)
166
+
167
+ # Peak gain (accounting for scan)
168
+ g_peak = directivity_db - scan_loss
169
+
170
+ # Beamwidth approximations for uniform rectangular array
171
+ # BW ≈ 0.886 * lambda / (N * d) in radians, for uniform taper
172
+ # With d in wavelengths: BW ≈ 0.886 / (N * d_lambda) radians
173
+ bw_az_rad = 0.886 / (arch.array.nx * arch.array.dx_lambda)
174
+ bw_el_rad = 0.886 / (arch.array.ny * arch.array.dy_lambda)
175
+
176
+ beamwidth_az_deg = np.degrees(bw_az_rad)
177
+ beamwidth_el_deg = np.degrees(bw_el_rad)
178
+
179
+ # Sidelobe level for uniform taper (theoretical: -13.2 dB)
180
+ sll_db = -13.2
181
+
182
+ return {
183
+ "g_peak_db": g_peak,
184
+ "beamwidth_az_deg": beamwidth_az_deg,
185
+ "beamwidth_el_deg": beamwidth_el_deg,
186
+ "sll_db": sll_db,
187
+ "scan_loss_db": scan_loss,
188
+ "directivity_db": directivity_db,
189
+ "n_elements": arch.array.n_elements,
190
+ }
@@ -0,0 +1,166 @@
1
+ """Antenna pattern metric extraction utilities."""
2
+
3
+ import math
4
+
5
+ import numpy as np
6
+ from numpy.typing import NDArray
7
+
8
+
9
+ def compute_beamwidth(
10
+ pattern_db: NDArray[np.floating],
11
+ angles_deg: NDArray[np.floating],
12
+ level_db: float = -3.0,
13
+ ) -> float:
14
+ """Compute beamwidth at specified level below peak.
15
+
16
+ Args:
17
+ pattern_db: Pattern magnitude in dB
18
+ angles_deg: Corresponding angles in degrees
19
+ level_db: Level below peak to measure (default -3 dB)
20
+
21
+ Returns:
22
+ Beamwidth in degrees, or NaN if not found
23
+ """
24
+ peak_db = np.max(pattern_db)
25
+ threshold = peak_db + level_db # level_db is negative
26
+
27
+ # Find peak index
28
+ peak_idx = np.argmax(pattern_db)
29
+
30
+ # Search left from peak
31
+ left_idx = peak_idx
32
+ for i in range(peak_idx, -1, -1):
33
+ if pattern_db[i] < threshold:
34
+ left_idx = i
35
+ break
36
+
37
+ # Search right from peak
38
+ right_idx = peak_idx
39
+ for i in range(peak_idx, len(pattern_db)):
40
+ if pattern_db[i] < threshold:
41
+ right_idx = i
42
+ break
43
+
44
+ if left_idx == peak_idx or right_idx == peak_idx:
45
+ return float("nan")
46
+
47
+ # Linear interpolation for more accurate crossing points
48
+ left_angle = np.interp(threshold, [pattern_db[left_idx], pattern_db[left_idx + 1]],
49
+ [angles_deg[left_idx], angles_deg[left_idx + 1]])
50
+ right_angle = np.interp(threshold, [pattern_db[right_idx], pattern_db[right_idx - 1]],
51
+ [angles_deg[right_idx], angles_deg[right_idx - 1]])
52
+
53
+ return abs(right_angle - left_angle)
54
+
55
+
56
+ def compute_sidelobe_level(
57
+ pattern_db: NDArray[np.floating],
58
+ angles_deg: NDArray[np.floating],
59
+ main_lobe_width_deg: float | None = None,
60
+ ) -> float:
61
+ """Compute peak sidelobe level relative to main beam.
62
+
63
+ Args:
64
+ pattern_db: Pattern magnitude in dB
65
+ angles_deg: Corresponding angles in degrees
66
+ main_lobe_width_deg: Width of main lobe to exclude (auto-detected if None)
67
+
68
+ Returns:
69
+ Peak sidelobe level in dB (negative value)
70
+ """
71
+ peak_db = np.max(pattern_db)
72
+ peak_idx = np.argmax(pattern_db)
73
+ peak_angle = angles_deg[peak_idx]
74
+
75
+ # Auto-detect main lobe width if not provided
76
+ if main_lobe_width_deg is None:
77
+ bw = compute_beamwidth(pattern_db, angles_deg, -3.0)
78
+ if np.isnan(bw):
79
+ bw = 10.0 # Default fallback
80
+ main_lobe_width_deg = bw * 2 # Use 2x beamwidth as exclusion zone
81
+
82
+ # Mask out main lobe region
83
+ half_width = main_lobe_width_deg / 2
84
+ mask = np.abs(angles_deg - peak_angle) > half_width
85
+
86
+ if not np.any(mask):
87
+ return float("-inf")
88
+
89
+ sidelobe_pattern = pattern_db[mask]
90
+ peak_sidelobe_db = np.max(sidelobe_pattern)
91
+
92
+ return peak_sidelobe_db - peak_db
93
+
94
+
95
+ def compute_scan_loss(scan_angle_deg: float, model: str = "cosine") -> float:
96
+ """Compute scan loss for a phased array at given scan angle.
97
+
98
+ Args:
99
+ scan_angle_deg: Scan angle from boresight (degrees)
100
+ model: Scan loss model ("cosine" or "cosine_squared")
101
+
102
+ Returns:
103
+ Scan loss in dB (positive value representing loss)
104
+ """
105
+ if scan_angle_deg >= 90:
106
+ return float("inf")
107
+
108
+ scan_rad = math.radians(scan_angle_deg)
109
+
110
+ if model == "cosine":
111
+ # Standard cos(theta) scan loss
112
+ loss_linear = math.cos(scan_rad)
113
+ elif model == "cosine_squared":
114
+ # More aggressive cos^2(theta) model
115
+ loss_linear = math.cos(scan_rad) ** 2
116
+ else:
117
+ raise ValueError(f"Unknown scan loss model: {model}")
118
+
119
+ if loss_linear <= 0:
120
+ return float("inf")
121
+
122
+ return -10 * math.log10(loss_linear)
123
+
124
+
125
+ def compute_array_gain(n_elements: int, element_gain_db: float = 0.0) -> float:
126
+ """Compute ideal array gain.
127
+
128
+ Args:
129
+ n_elements: Number of array elements
130
+ element_gain_db: Individual element gain (dB)
131
+
132
+ Returns:
133
+ Array gain in dB
134
+ """
135
+ if n_elements < 1:
136
+ raise ValueError("n_elements must be >= 1")
137
+
138
+ array_factor_db = 10 * math.log10(n_elements)
139
+ return element_gain_db + array_factor_db
140
+
141
+
142
+ def compute_directivity_rectangular(
143
+ nx: int, ny: int, dx_lambda: float, dy_lambda: float
144
+ ) -> float:
145
+ """Estimate directivity for a rectangular array.
146
+
147
+ Uses the approximation: D = pi * (2*nx*dx) * (2*ny*dy) for large arrays.
148
+
149
+ Args:
150
+ nx: Number of elements in x
151
+ ny: Number of elements in y
152
+ dx_lambda: Element spacing in x (wavelengths)
153
+ dy_lambda: Element spacing in y (wavelengths)
154
+
155
+ Returns:
156
+ Directivity in dB
157
+ """
158
+ # Aperture dimensions in wavelengths
159
+ lx = nx * dx_lambda
160
+ ly = ny * dy_lambda
161
+
162
+ # Directivity approximation for uniform aperture
163
+ # D ≈ 4*pi*A/lambda^2 = 4*pi*Lx*Ly (when Lx, Ly in wavelengths)
164
+ directivity_linear = 4 * math.pi * lx * ly
165
+
166
+ return 10 * math.log10(directivity_linear)
@@ -0,0 +1,30 @@
1
+ """Base model block protocol and utilities."""
2
+
3
+ from typing import Any, Protocol, runtime_checkable
4
+
5
+ from phased_array_systems.types import MetricsDict, Scenario
6
+
7
+
8
+ @runtime_checkable
9
+ class ModelBlock(Protocol):
10
+ """Protocol for model blocks that evaluate architecture/scenario combinations.
11
+
12
+ All model blocks must implement evaluate() and return a flat metrics dictionary.
13
+ The metrics dictionary uses the canonical metric keys defined in the package
14
+ documentation.
15
+ """
16
+
17
+ name: str
18
+
19
+ def evaluate(self, arch: Any, scenario: Scenario, context: dict[str, Any]) -> MetricsDict:
20
+ """Evaluate the model and return metrics.
21
+
22
+ Args:
23
+ arch: Architecture configuration object
24
+ scenario: Scenario configuration object
25
+ context: Additional context (e.g., results from other models)
26
+
27
+ Returns:
28
+ Dictionary of metric_name -> value pairs
29
+ """
30
+ ...
@@ -0,0 +1,9 @@
1
+ """Communications link budget models."""
2
+
3
+ from phased_array_systems.models.comms.link_budget import CommsLinkModel
4
+ from phased_array_systems.models.comms.propagation import compute_fspl
5
+
6
+ __all__ = [
7
+ "CommsLinkModel",
8
+ "compute_fspl",
9
+ ]