solarc-eclipse 0.6.1__tar.gz → 0.6.1.2__tar.gz

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 (35) hide show
  1. {solarc_eclipse-0.6.1/solarc_eclipse.egg-info → solarc_eclipse-0.6.1.2}/PKG-INFO +11 -4
  2. {solarc_eclipse-0.6.1 → solarc_eclipse-0.6.1.2}/README.md +9 -0
  3. {solarc_eclipse-0.6.1 → solarc_eclipse-0.6.1.2}/euvst_response/__init__.py +5 -4
  4. {solarc_eclipse-0.6.1 → solarc_eclipse-0.6.1.2}/euvst_response/analysis.py +48 -23
  5. {solarc_eclipse-0.6.1 → solarc_eclipse-0.6.1.2}/euvst_response/config.py +10 -6
  6. {solarc_eclipse-0.6.1 → solarc_eclipse-0.6.1.2}/euvst_response/data_processing.py +53 -15
  7. solarc_eclipse-0.6.1.2/euvst_response/fitting.py +399 -0
  8. {solarc_eclipse-0.6.1 → solarc_eclipse-0.6.1.2}/euvst_response/main.py +37 -5
  9. {solarc_eclipse-0.6.1 → solarc_eclipse-0.6.1.2}/euvst_response/monte_carlo.py +19 -13
  10. {solarc_eclipse-0.6.1 → solarc_eclipse-0.6.1.2}/euvst_response/radiometric.py +103 -70
  11. {solarc_eclipse-0.6.1 → solarc_eclipse-0.6.1.2}/euvst_response/utils.py +19 -0
  12. {solarc_eclipse-0.6.1 → solarc_eclipse-0.6.1.2}/pyproject.toml +1 -3
  13. {solarc_eclipse-0.6.1 → solarc_eclipse-0.6.1.2}/setup.py +1 -3
  14. {solarc_eclipse-0.6.1 → solarc_eclipse-0.6.1.2/solarc_eclipse.egg-info}/PKG-INFO +11 -4
  15. solarc_eclipse-0.6.1/euvst_response/fitting.py +0 -144
  16. {solarc_eclipse-0.6.1 → solarc_eclipse-0.6.1.2}/LICENSE +0 -0
  17. {solarc_eclipse-0.6.1 → solarc_eclipse-0.6.1.2}/MANIFEST.in +0 -0
  18. {solarc_eclipse-0.6.1 → solarc_eclipse-0.6.1.2}/euvst_response/cli.py +0 -0
  19. {solarc_eclipse-0.6.1 → solarc_eclipse-0.6.1.2}/euvst_response/data/throughput/grating_reflection_efficiency.dat +0 -0
  20. {solarc_eclipse-0.6.1 → solarc_eclipse-0.6.1.2}/euvst_response/data/throughput/primary_mirror_coating_reflectance.dat +0 -0
  21. {solarc_eclipse-0.6.1 → solarc_eclipse-0.6.1.2}/euvst_response/data/throughput/source.txt +0 -0
  22. {solarc_eclipse-0.6.1 → solarc_eclipse-0.6.1.2}/euvst_response/data/throughput/throughput_aluminium_1000_angstrom.dat +0 -0
  23. {solarc_eclipse-0.6.1 → solarc_eclipse-0.6.1.2}/euvst_response/data/throughput/throughput_aluminium_oxide_1000_angstrom.dat +0 -0
  24. {solarc_eclipse-0.6.1 → solarc_eclipse-0.6.1.2}/euvst_response/data/throughput/throughput_carbon_1000_angstrom.dat +0 -0
  25. {solarc_eclipse-0.6.1 → solarc_eclipse-0.6.1.2}/euvst_response/pinhole_diffraction.py +0 -0
  26. {solarc_eclipse-0.6.1 → solarc_eclipse-0.6.1.2}/euvst_response/psf.py +0 -0
  27. {solarc_eclipse-0.6.1 → solarc_eclipse-0.6.1.2}/euvst_response/synthesis.py +0 -0
  28. {solarc_eclipse-0.6.1 → solarc_eclipse-0.6.1.2}/euvst_response/synthesis_cli.py +0 -0
  29. {solarc_eclipse-0.6.1 → solarc_eclipse-0.6.1.2}/setup.cfg +0 -0
  30. {solarc_eclipse-0.6.1 → solarc_eclipse-0.6.1.2}/solarc_eclipse.egg-info/SOURCES.txt +0 -0
  31. {solarc_eclipse-0.6.1 → solarc_eclipse-0.6.1.2}/solarc_eclipse.egg-info/dependency_links.txt +0 -0
  32. {solarc_eclipse-0.6.1 → solarc_eclipse-0.6.1.2}/solarc_eclipse.egg-info/entry_points.txt +0 -0
  33. {solarc_eclipse-0.6.1 → solarc_eclipse-0.6.1.2}/solarc_eclipse.egg-info/not-zip-safe +0 -0
  34. {solarc_eclipse-0.6.1 → solarc_eclipse-0.6.1.2}/solarc_eclipse.egg-info/requires.txt +0 -0
  35. {solarc_eclipse-0.6.1 → solarc_eclipse-0.6.1.2}/solarc_eclipse.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: solarc-eclipse
3
- Version: 0.6.1
3
+ Version: 0.6.1.2
4
4
  Summary: ECLIPSE: Emission Calculation and Line Prediction for SOLAR-C EUVST
5
5
  Home-page: https://github.com/jamesmckevitt/eclipse
6
6
  Author: James McKevitt
@@ -12,11 +12,9 @@ Classifier: Development Status :: 4 - Beta
12
12
  Classifier: Intended Audience :: Science/Research
13
13
  Classifier: Topic :: Scientific/Engineering :: Astronomy
14
14
  Classifier: Programming Language :: Python :: 3
15
- Classifier: Programming Language :: Python :: 3.8
16
- Classifier: Programming Language :: Python :: 3.9
17
15
  Classifier: Programming Language :: Python :: 3.10
18
16
  Classifier: Programming Language :: Python :: 3.11
19
- Requires-Python: >=3.8
17
+ Requires-Python: >=3.10
20
18
  Description-Content-Type: text/markdown
21
19
  License-File: LICENSE
22
20
  Requires-Dist: numpy
@@ -361,6 +359,15 @@ ccd_temperature: -60 Celsius # Default (expected operating temperature)
361
359
 
362
360
  # Visible stray light level
363
361
  vis_sl: 0 photon / (s * pixel) # Default, (ideal case, no stray light)
362
+
363
+ # Multi-component Gaussian fitting (optional, omit for single-Gaussian)
364
+ fitting:
365
+ primary_component: 0
366
+ components:
367
+ - wavelength: 195.119 angstrom
368
+ - wavelength: 195.179 angstrom
369
+ tie_center: 0 # Centroid offset tied to component 0
370
+ tie_width: 0 # Same width as component 0
364
371
  ```
365
372
 
366
373
  For guidance on recommended values, see McKevitt et al. (2025) (in prep.).
@@ -318,6 +318,15 @@ ccd_temperature: -60 Celsius # Default (expected operating temperature)
318
318
 
319
319
  # Visible stray light level
320
320
  vis_sl: 0 photon / (s * pixel) # Default, (ideal case, no stray light)
321
+
322
+ # Multi-component Gaussian fitting (optional, omit for single-Gaussian)
323
+ fitting:
324
+ primary_component: 0
325
+ components:
326
+ - wavelength: 195.119 angstrom
327
+ - wavelength: 195.179 angstrom
328
+ tie_center: 0 # Centroid offset tied to component 0
329
+ tie_width: 0 # Same width as component 0
321
330
  ```
322
331
 
323
332
  For guidance on recommended values, see McKevitt et al. (2025) (in prep.).
@@ -4,7 +4,7 @@ ECLIPSE: Emission Calculation and Line Prediction for SOLAR-C EUVST
4
4
  This package provides tools for modeling the performance of the EUV spectrograph EUVST, on SOLAR-C.
5
5
  """
6
6
 
7
- __version__ = "0.6.1"
7
+ __version__ = "0.6.1.2"
8
8
  __author__ = "James McKevitt"
9
9
  __email__ = "jm2@mssl.ucl.ac.uk"
10
10
 
@@ -13,11 +13,11 @@ from .config import Detector_SWC, Detector_EIS, Telescope_EUVST, Telescope_EIS,
13
13
  from .utils import wl_to_vel, vel_to_wl, angle_to_distance, distance_to_angle
14
14
  from .radiometric import (
15
15
  intensity_to_photons, add_telescope_throughput, photons_to_pixel_counts,
16
- apply_exposure_and_poisson, add_poisson, apply_focusing_optics_psf, to_electrons,
16
+ apply_exposure, sample_photon_arrivals, add_poisson, apply_focusing_optics_psf, to_electrons,
17
17
  to_dn, add_visible_stray_light, add_pinhole_visible_light
18
18
  )
19
19
  from .pinhole_diffraction import apply_euv_pinhole_diffraction, airy_disk_pattern
20
- from .fitting import fit_cube_gauss, velocity_from_fit, width_from_fit, analyse
20
+ from .fitting import fit_cube_gauss, velocity_from_fit, width_from_fit, analyse, FitConfig, FitComponent
21
21
  from .monte_carlo import simulate_once, monte_carlo
22
22
  from .main import main
23
23
  from .data_processing import load_atmosphere
@@ -36,9 +36,10 @@ __all__ = [
36
36
  "Simulation", "AluminiumFilter",
37
37
  "wl_to_vel", "vel_to_wl", "angle_to_distance", "distance_to_angle",
38
38
  "intensity_to_photons", "add_telescope_throughput", "photons_to_pixel_counts",
39
- "apply_exposure_and_poisson", "add_poisson", "apply_focusing_optics_psf", "to_electrons",
39
+ "apply_exposure", "sample_photon_arrivals", "add_poisson", "apply_focusing_optics_psf", "to_electrons",
40
40
  "to_dn", "add_visible_stray_light", "add_pinhole_visible_light",
41
41
  "fit_cube_gauss", "velocity_from_fit", "width_from_fit", "analyse",
42
+ "FitConfig", "FitComponent",
42
43
  "simulate_once", "monte_carlo",
43
44
  "main",
44
45
  "load_atmosphere",
@@ -112,7 +112,8 @@ def get_parameter_combinations(results: Dict[str, Any]) -> List[Tuple]:
112
112
  def analyse_fit_statistics(
113
113
  combination_results: Dict[str, Any],
114
114
  rest_wavelength: u.Quantity,
115
- data_type: str = "dn"
115
+ data_type: str = "dn",
116
+ fit_config=None,
116
117
  ) -> Dict[str, Any]:
117
118
  """
118
119
  Analyze fit statistics to compute velocity and line width statistics.
@@ -125,12 +126,24 @@ def analyse_fit_statistics(
125
126
  Rest wavelength for velocity conversion.
126
127
  data_type : str, optional
127
128
  Either "dn" or "photon" to specify which fit statistics to analyze.
129
+ fit_config : FitConfig, optional
130
+ Multi-component fitting configuration. When provided the primary-
131
+ component indices are used; otherwise indices 1 (centre) and
132
+ 2 (sigma) are assumed (single-component default).
128
133
 
129
134
  Returns
130
135
  -------
131
136
  dict
132
137
  Dictionary containing velocity and width statistics.
133
138
  """
139
+ # Determine parameter indices for the primary component
140
+ if fit_config is not None and not fit_config.is_single:
141
+ idx_center = fit_config.idx_center
142
+ idx_sigma = fit_config.idx_sigma
143
+ else:
144
+ idx_center = 1
145
+ idx_sigma = 2
146
+
134
147
  # Get fit statistics
135
148
  fit_stats_key = f"{data_type}_fit_stats"
136
149
  if fit_stats_key not in combination_results:
@@ -141,19 +154,19 @@ def analyse_fit_statistics(
141
154
  fit_truth_units = combination_results["ground_truth"]["fit_truth_units"]
142
155
 
143
156
  # Extract data and units
144
- mean_data = fit_stats["mean_data"] # Shape: (nx, ny, 4)
145
- std_data = fit_stats["std_data"] # Shape: (nx, ny, 4)
146
- units = fit_stats["units"] # List of 4 astropy units
157
+ mean_data = fit_stats["mean_data"] # Shape: (nx, ny, n_params)
158
+ std_data = fit_stats["std_data"] # Shape: (nx, ny, n_params)
159
+ units = fit_stats["units"] # List of n_params astropy units
147
160
 
148
- # Get center statistics (parameter index 1)
149
- center_mean_data = mean_data[..., 1] # (nx, ny) - values only
150
- center_std_data = std_data[..., 1] # (nx, ny) - values only
151
- center_unit = units[1] # wavelength unit
161
+ # Get center statistics for the primary component
162
+ center_mean_data = mean_data[..., idx_center]
163
+ center_std_data = std_data[..., idx_center]
164
+ center_unit = units[idx_center]
152
165
 
153
- # Get width statistics (parameter index 2)
154
- width_mean_data = mean_data[..., 2] # (nx, ny) - values only
155
- width_std_data = std_data[..., 2] # (nx, ny) - values only
156
- width_unit = units[2] # wavelength unit
166
+ # Get width statistics for the primary component
167
+ width_mean_data = mean_data[..., idx_sigma]
168
+ width_std_data = std_data[..., idx_sigma]
169
+ width_unit = units[idx_sigma]
157
170
 
158
171
  # Create quantities
159
172
  center_mean_q = center_mean_data * center_unit
@@ -170,7 +183,7 @@ def analyse_fit_statistics(
170
183
 
171
184
  # Convert to velocities
172
185
  v_mean = centers_to_velocity(center_mean_q, rest_wavelength)
173
- v_true = centers_to_velocity(fit_truth_data[..., 1] * fit_truth_units[1], rest_wavelength)
186
+ v_true = centers_to_velocity(fit_truth_data[..., idx_center] * fit_truth_units[idx_center], rest_wavelength)
174
187
  v_err = v_true - v_mean
175
188
 
176
189
  # Convert center std to velocity std using differential: dv/dlambda = c/lambda
@@ -459,7 +472,8 @@ def create_sunpy_maps_from_combo(
459
472
  rest_wavelength: u.Quantity = 195.119 * u.AA,
460
473
  data_type: str = "dn",
461
474
  precision_requirement: u.Quantity = 2.0 * u.km / u.s,
462
- exposure_time_results: List[Dict[str, Any]] | None = None
475
+ exposure_time_results: List[Dict[str, Any]] | None = None,
476
+ fit_config=None,
463
477
  ) -> Dict[str, Any]:
464
478
  """
465
479
  Create SunPy maps from combination results using the new fit statistics structure.
@@ -479,6 +493,9 @@ def create_sunpy_maps_from_combo(
479
493
  exposure_time_results : list of dict, optional
480
494
  List of results from get_results_for_combination() for different exposure times.
481
495
  If provided, will create an exposure time map showing minimum exposure needed.
496
+ fit_config : FitConfig, optional
497
+ Multi-component fitting configuration. When provided, the primary-
498
+ component indices are used to extract centre and width parameters.
482
499
 
483
500
  Returns
484
501
  -------
@@ -504,7 +521,7 @@ def create_sunpy_maps_from_combo(
504
521
  # Extract exposure time from parameters
505
522
  exposure_time = result["parameters"]["exposure"].to_value(u.s)
506
523
  # Create analysis for this exposure
507
- analysis = analyse_fit_statistics(result, rest_wavelength, data_type)
524
+ analysis = analyse_fit_statistics(result, rest_wavelength, data_type, fit_config=fit_config)
508
525
  analysis_per_exp[exposure_time] = analysis
509
526
  else:
510
527
  analysis_per_exp = None
@@ -535,14 +552,22 @@ def create_sunpy_maps_from_combo(
535
552
  maps['total_dn'] = sunpy.map.Map(total_dn_data.T, wcs_2d)
536
553
  maps['total_dn'].meta['bunit'] = str(total_dn_unit)
537
554
 
555
+ # Determine parameter indices for the primary component
556
+ if fit_config is not None and not fit_config.is_single:
557
+ idx_center = fit_config.idx_center
558
+ idx_sigma = fit_config.idx_sigma
559
+ else:
560
+ idx_center = 1
561
+ idx_sigma = 2
562
+
538
563
  # --- Get velocity and width analysis for this combination ---
539
- analysis = analyse_fit_statistics(combination_results, rest_wavelength, data_type)
564
+ analysis = analyse_fit_statistics(combination_results, rest_wavelength, data_type, fit_config=fit_config)
540
565
 
541
566
  # --- Velocity maps ---
542
- # Velocity from first fit (parameter 1 = center)
543
- first_fit_data = fit_stats["first_fit_data"] # Shape: (nx, ny, 4)
544
- center_first_data = first_fit_data[..., 1] # Extract center parameter
545
- center_first_unit = fit_stats["units"][1] # Get units for center parameter
567
+ # Velocity from first fit (primary component center)
568
+ first_fit_data = fit_stats["first_fit_data"] # Shape: (nx, ny, n_params)
569
+ center_first_data = first_fit_data[..., idx_center]
570
+ center_first_unit = fit_stats["units"][idx_center]
546
571
 
547
572
  def centers_to_velocity(centers_data, centers_unit, lambda0):
548
573
  """Convert wavelength centers to velocities"""
@@ -570,9 +595,9 @@ def create_sunpy_maps_from_combo(
570
595
  maps['velocity_err'].meta['bunit'] = str(analysis["v_err"].unit)
571
596
 
572
597
  # --- Line width maps ---
573
- # Line width from first fit (parameter 2 = width)
574
- width_first_data = first_fit_data[..., 2] # Extract width parameter data
575
- width_first_unit = fit_stats["units"][2] # Get units for width parameter
598
+ # Line width from first fit (primary component sigma)
599
+ width_first_data = first_fit_data[..., idx_sigma]
600
+ width_first_unit = fit_stats["units"][idx_sigma]
576
601
 
577
602
  # Create quantity with proper units
578
603
  width_quantity = width_first_data * width_first_unit
@@ -243,7 +243,8 @@ class Telescope_EUVST:
243
243
  microroughness_sigma: u.Quantity = 0.3 * u.nm # RMS microroughness for primary mirror
244
244
  filter: AluminiumFilter = field(default_factory=AluminiumFilter)
245
245
  psf_type: str = "gaussian"
246
- psf_params: list = field(default_factory=lambda: [0.343 * u.pixel]) # FWHM of 0.805 pix from 0.128 arcsec from optical design RSC-2022021B in sigma
246
+ # psf_params: list = field(default_factory=lambda: [1.26 * u.pixel, 1.95 * u.pixel]) # [spatial_fwhm, spectral_fwhm] in pixels. From 0.200 arcsec (w/ slit-scan; FOV2) and 33.00 mA in RSC-2022021 (Oct 2023) and RSC-2022021B (Feb 2024).
247
+ psf_params: list = field(default_factory=lambda: [2.66 * u.pixel, 2.54 * u.pixel]) # [spatial_fwhm, spectral_fwhm] in pixels. From 0.423 arcsec (w/ slit-scan; FOV2) and 43.00 mA in RSC-2022021C (Mar 2025).
247
248
 
248
249
  # Wavelength-dependent efficiency tables
249
250
  pm_table: Path = field(default_factory=lambda: files('euvst_response') / 'data' / 'throughput' / 'primary_mirror_coating_reflectance.dat')
@@ -251,7 +252,7 @@ class Telescope_EUVST:
251
252
 
252
253
  @property
253
254
  def collecting_area(self) -> u.Quantity:
254
- return 0.5 * np.pi * (self.D_ap / 2) ** 2
255
+ return 0.5 * np.pi * (self.D_ap / 2) ** 2 # Accounting for 50% loss due to beam division between SW and LW channels.
255
256
 
256
257
  def primary_mirror_efficiency(self, wl0: u.Quantity) -> float:
257
258
  """
@@ -293,7 +294,10 @@ class Telescope_EUVST:
293
294
  """
294
295
  Calculate the efficiency reduction due to primary mirror microroughness.
295
296
 
296
- Formula: 1 - (4*pi*sigma/lambda)^2
297
+ Uses the Debye-Waller factor for specular reflectance:
298
+
299
+ efficiency = exp(-(4*pi*sigma/lambda)^2)
300
+
297
301
  where sigma is the RMS microroughness and lambda is the wavelength.
298
302
 
299
303
  Parameters
@@ -313,8 +317,8 @@ class Telescope_EUVST:
313
317
  # Calculate (4*pi*sigma/lambda)^2
314
318
  roughness_term = (4 * np.pi * sigma_nm / wl_nm) ** 2
315
319
 
316
- # Return 1 - (4*pi*sigma/lambda)^2
317
- return 1.0 - roughness_term.value
320
+ # Return exp(-(4*pi*sigma/lambda)^2) [Debye-Waller specular efficiency]
321
+ return np.exp(-roughness_term.value)
318
322
 
319
323
  def throughput(self, wl0: u.Quantity) -> float:
320
324
  """
@@ -348,7 +352,7 @@ class Telescope_EUVST:
348
352
  class Telescope_EIS:
349
353
  """Hinode/EIS telescope configuration for comparison."""
350
354
  psf_type: str = "gaussian"
351
- psf_params: list = field(default_factory=lambda: [1.28 * u.pixel]) # FWHM of 3 in sigma
355
+ psf_params: list = field(default_factory=lambda: [3.0 * u.pixel, 3.0 * u.pixel]) # [spatial_fwhm, spectral_fwhm] in pixels
352
356
 
353
357
  def ea_and_throughput(self, wl0: u.Quantity) -> u.Quantity:
354
358
  # Effective area including detector QE is 0.23 cm2
@@ -152,18 +152,37 @@ def resample_ndcube_spectral_axis(ndcube, spectral_axis, output_resolution, ncpu
152
152
  data = np.moveaxis(ndcube.data, spectral_axis, -1)
153
153
  shape = data.shape
154
154
  flat_data = data.reshape(-1, shape[-1])
155
-
156
- resampler = FluxConservingResampler(extrapolation_treatment="zero_fill")
157
- resampled = np.zeros((flat_data.shape[0], n_spec))
158
-
159
- def _resample_pixel(i):
160
- spec = Spectrum(flux=flat_data[i] * ndcube.unit, spectral_axis=spectral_world)
161
- res = resampler(spec, new_spec_grid)
162
- return res.flux.value
163
-
164
- with tqdm_joblib(tqdm(total=flat_data.shape[0], desc="Resampling spectral axis", unit="pixel", leave=False)):
155
+ n_pixels = flat_data.shape[0]
156
+
157
+ # Determine number of workers
158
+ import os
159
+ if ncpu == -1:
160
+ n_workers = os.cpu_count() or 1
161
+ else:
162
+ n_workers = ncpu
163
+
164
+ # Calculate batch size: aim for ~4 batches per worker to balance load
165
+ # but ensure each batch has enough work to justify overhead
166
+ min_pixels_per_batch = 100
167
+ n_batches = max(1, min(n_pixels // min_pixels_per_batch, n_workers * 4))
168
+ batch_size = (n_pixels + n_batches - 1) // n_batches # ceiling division
169
+
170
+ # Create batch indices
171
+ batch_indices = [(i, min(i + batch_size, n_pixels)) for i in range(0, n_pixels, batch_size)]
172
+
173
+ def _resample_batch(start_idx, end_idx):
174
+ """Resample a batch of pixels."""
175
+ resampler = FluxConservingResampler(extrapolation_treatment="zero_fill")
176
+ batch_results = np.empty((end_idx - start_idx, n_spec))
177
+ for i, pixel_idx in enumerate(range(start_idx, end_idx)):
178
+ spec = Spectrum(flux=flat_data[pixel_idx] * ndcube.unit, spectral_axis=spectral_world)
179
+ res = resampler(spec, new_spec_grid)
180
+ batch_results[i] = res.flux.value
181
+ return batch_results
182
+
183
+ with tqdm_joblib(tqdm(total=len(batch_indices), desc="Resampling spectral axis", unit="batch", leave=False)):
165
184
  results = Parallel(n_jobs=ncpu)(
166
- delayed(_resample_pixel)(i) for i in range(flat_data.shape[0])
185
+ delayed(_resample_batch)(start, end) for start, end in batch_indices
167
186
  )
168
187
  resampled = np.vstack(results)
169
188
 
@@ -186,8 +205,21 @@ def resample_ndcube_spectral_axis(ndcube, spectral_axis, output_resolution, ncpu
186
205
  return NDCube(resampled, wcs=new_wcs, unit=ndcube.unit, meta=ndcube.meta)
187
206
 
188
207
 
189
- def reproject_ndcube_heliocentric_to_helioprojective(new_cube_spec, sim, det):
190
- """ Reproject an NDCube from heliocentric to helioprojective coordinates. """
208
+ def reproject_ndcube_heliocentric_to_helioprojective(new_cube_spec, sim, det, ncpu=-1):
209
+ """ Reproject an NDCube from heliocentric to helioprojective coordinates.
210
+
211
+ Parameters
212
+ ----------
213
+ new_cube_spec : NDCube
214
+ Input NDCube in heliocentric coordinates
215
+ sim : Simulation
216
+ Simulation configuration object
217
+ det : Detector
218
+ Detector configuration object
219
+ ncpu : int, optional
220
+ Number of CPU cores for parallel reprojection. -1 uses all cores,
221
+ positive integers specify exact count. Default is -1.
222
+ """
191
223
 
192
224
  nx, ny, _ = new_cube_spec.shape
193
225
  wcs_hc = new_cube_spec.wcs
@@ -234,11 +266,16 @@ def reproject_ndcube_heliocentric_to_helioprojective(new_cube_spec, sim, det):
234
266
  (det.plate_scale_angle * u.pix).to_value(u.arcsec),
235
267
  (sim.slit_width).to_value(u.arcsec)]
236
268
 
269
+ # Determine parallelization setting:
270
+ # - If ncpu=-1, use True (all available cores)
271
+ # - If ncpu is a positive integer, pass it directly to control thread count
272
+ parallel_setting = True if ncpu == -1 else ncpu
273
+
237
274
  new_cube_spec_hp_spat = new_cube_spec_hp.reproject_to(
238
275
  wcs_tgt,
239
276
  shape_out=shape_out,
240
277
  algorithm='interpolation',
241
- parallel=True,
278
+ parallel=parallel_setting,
242
279
  order='bilinear',
243
280
  ) * new_cube_spec_hp.unit
244
281
 
@@ -273,7 +310,8 @@ def rebin_atmosphere(cube_sim, det, sim, use_dask=False):
273
310
  cube_det = reproject_ndcube_heliocentric_to_helioprojective(
274
311
  cube_spec,
275
312
  sim,
276
- det
313
+ det,
314
+ ncpu=sim.ncpu
277
315
  )
278
316
 
279
317
  return cube_det