RunFeemsSim 0.5.0__tar.gz → 0.7.0__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.
- {runfeemssim-0.5.0 → runfeemssim-0.7.0}/PKG-INFO +1 -1
- {runfeemssim-0.5.0 → runfeemssim-0.7.0}/RunFeemsSim/machinery_calculation.py +70 -2
- {runfeemssim-0.5.0 → runfeemssim-0.7.0}/RunFeemsSim.egg-info/PKG-INFO +1 -1
- {runfeemssim-0.5.0 → runfeemssim-0.7.0}/pyproject.toml +1 -1
- {runfeemssim-0.5.0 → runfeemssim-0.7.0}/tests/test_machinery_calculation.py +208 -0
- {runfeemssim-0.5.0 → runfeemssim-0.7.0}/LICENSE +0 -0
- {runfeemssim-0.5.0 → runfeemssim-0.7.0}/README.md +0 -0
- {runfeemssim-0.5.0 → runfeemssim-0.7.0}/RunFeemsSim/__init__.py +0 -0
- {runfeemssim-0.5.0 → runfeemssim-0.7.0}/RunFeemsSim/pms_basic.py +0 -0
- {runfeemssim-0.5.0 → runfeemssim-0.7.0}/RunFeemsSim.egg-info/SOURCES.txt +0 -0
- {runfeemssim-0.5.0 → runfeemssim-0.7.0}/RunFeemsSim.egg-info/dependency_links.txt +0 -0
- {runfeemssim-0.5.0 → runfeemssim-0.7.0}/RunFeemsSim.egg-info/requires.txt +0 -0
- {runfeemssim-0.5.0 → runfeemssim-0.7.0}/RunFeemsSim.egg-info/top_level.txt +0 -0
- {runfeemssim-0.5.0 → runfeemssim-0.7.0}/setup.cfg +0 -0
- {runfeemssim-0.5.0 → runfeemssim-0.7.0}/tests/test_pms_basic.py +0 -0
|
@@ -290,6 +290,25 @@ class MachineryCalculation:
|
|
|
290
290
|
fuel_option=fuel_option,
|
|
291
291
|
)
|
|
292
292
|
|
|
293
|
+
def _set_boiler_steam_demand(self, steam_demand_kg_per_h: Optional[np.ndarray]) -> None:
|
|
294
|
+
"""Set the boiler steam demand aligned to the simulation's integration intervals.
|
|
295
|
+
|
|
296
|
+
The number of integration points equals ``len(time_interval_s)``, which is one less
|
|
297
|
+
than the number of timestamps when the load is given as a timestamp series (the last
|
|
298
|
+
point is dropped, mirroring how the propulsion power is handled). The steam demand is
|
|
299
|
+
therefore truncated to the same number of points so the boiler fuel integration matches
|
|
300
|
+
the engine integration. No-op when the system has no boiler.
|
|
301
|
+
"""
|
|
302
|
+
if self.system_feems.boiler is None:
|
|
303
|
+
return
|
|
304
|
+
n_points = len(np.atleast_1d(self.electric_system.time_interval_s))
|
|
305
|
+
if steam_demand_kg_per_h is None:
|
|
306
|
+
self.system_feems.boiler.steam_out_kg_per_h = np.zeros(n_points)
|
|
307
|
+
else:
|
|
308
|
+
self.system_feems.boiler.steam_out_kg_per_h = (
|
|
309
|
+
np.asarray(steam_demand_kg_per_h, dtype=float)[:n_points]
|
|
310
|
+
)
|
|
311
|
+
|
|
293
312
|
def calculate_machinery_system_output_from_propulsion_power_time_series(
|
|
294
313
|
self,
|
|
295
314
|
*,
|
|
@@ -298,6 +317,7 @@ class MachineryCalculation:
|
|
|
298
317
|
fuel_specified_by: FuelSpecifiedBy = FuelSpecifiedBy.IMO,
|
|
299
318
|
ignore_power_balance: bool = False,
|
|
300
319
|
fuel_option: Optional[FuelOption] = None,
|
|
320
|
+
steam_demand_kg_per_h: Optional[np.ndarray] = None,
|
|
301
321
|
) -> Union[FEEMSResult, FEEMSResultForMachinerySystem]:
|
|
302
322
|
"""
|
|
303
323
|
Calculate the machinery system output from a time series of the propulsion power and
|
|
@@ -313,6 +333,8 @@ class MachineryCalculation:
|
|
|
313
333
|
ignore_power_balance(bool): If True, the power balance calculation will be ignored.
|
|
314
334
|
fuel_option(FuelOption): The fuel option to be used in the simulation. If None, the
|
|
315
335
|
default fuel option in the system will be used.
|
|
336
|
+
steam_demand_kg_per_h: Steam demand time series (kg/h). Must have the same length as
|
|
337
|
+
propulsion_power. If None and a boiler is attached, demand is treated as zero.
|
|
316
338
|
|
|
317
339
|
Returns:
|
|
318
340
|
The result of the calculation. FEEMSResult or FEEMSResultForMachinerySystem.
|
|
@@ -347,11 +369,19 @@ class MachineryCalculation:
|
|
|
347
369
|
f"The length of the auxiliary power({len(auxiliary_power_kw)}) must be 1"
|
|
348
370
|
f" or the same as the propulsion power ({len(propulsion_power)})"
|
|
349
371
|
)
|
|
372
|
+
n_steps = len(propulsion_power)
|
|
373
|
+
if steam_demand_kg_per_h is not None:
|
|
374
|
+
if len(steam_demand_kg_per_h) != n_steps:
|
|
375
|
+
raise ValueError(
|
|
376
|
+
f"The length of steam_demand_kg_per_h ({len(steam_demand_kg_per_h)}) must match "
|
|
377
|
+
f"propulsion_power ({n_steps})."
|
|
378
|
+
)
|
|
350
379
|
|
|
351
380
|
self._set_input_load_time_interval_from_propulsion_power_time_series(
|
|
352
381
|
propulsion_power_time_series=propulsion_power,
|
|
353
382
|
auxiliary_load_kw=auxiliary_power_kw,
|
|
354
383
|
)
|
|
384
|
+
self._set_boiler_steam_demand(steam_demand_kg_per_h)
|
|
355
385
|
return self._run_simulation(
|
|
356
386
|
fuel_specified_by=fuel_specified_by,
|
|
357
387
|
ignore_power_balance=ignore_power_balance,
|
|
@@ -368,6 +398,7 @@ class MachineryCalculation:
|
|
|
368
398
|
fuel_specified_by: FuelSpecifiedBy = FuelSpecifiedBy.IMO,
|
|
369
399
|
ignore_power_balance: bool = False,
|
|
370
400
|
fuel_option: Optional[FuelOption] = None,
|
|
401
|
+
steam_demand_kg_per_h: Optional[np.ndarray] = None,
|
|
371
402
|
) -> Union[FEEMSResult, FEEMSResultForMachinerySystem]:
|
|
372
403
|
"""
|
|
373
404
|
Calculate the machinery system output from statistics of the propulsion power.
|
|
@@ -377,6 +408,11 @@ class MachineryCalculation:
|
|
|
377
408
|
ignore_power_balance(bool): If True, the power balance calculation will be ignored.
|
|
378
409
|
fuel_option(FuelOption): The fuel option to be used in the simulation. If None, the
|
|
379
410
|
default fuel option in the system will be used.
|
|
411
|
+
steam_demand_kg_per_h: Steam demand time series (kg/h). Must have the same length as
|
|
412
|
+
the time series. Takes precedence over the boiler_steam_demand_kg_per_h carried by
|
|
413
|
+
the time_series proto; if None, that proto value is used instead (per-instance, or
|
|
414
|
+
the top-level constant when the per-instance values are all zero). If both are
|
|
415
|
+
absent and a boiler is attached, demand is treated as zero.
|
|
380
416
|
|
|
381
417
|
Returns:
|
|
382
418
|
The result of the simulation. FEEMSResult or FEEMSResultForMachinery system.
|
|
@@ -398,7 +434,25 @@ class MachineryCalculation:
|
|
|
398
434
|
raise TypeError(
|
|
399
435
|
"time_series must be a TimeSeriesResult or TimeSeriesResultForMultiplePropulsors."
|
|
400
436
|
)
|
|
401
|
-
|
|
437
|
+
n_steps = len(df)
|
|
438
|
+
if steam_demand_kg_per_h is not None:
|
|
439
|
+
if len(steam_demand_kg_per_h) != n_steps:
|
|
440
|
+
raise ValueError(
|
|
441
|
+
f"The length of steam_demand_kg_per_h ({len(steam_demand_kg_per_h)}) must match "
|
|
442
|
+
f"the time series ({n_steps})."
|
|
443
|
+
)
|
|
444
|
+
# The explicit steam_demand_kg_per_h argument takes precedence; otherwise fall back to the
|
|
445
|
+
# boiler_steam_demand_kg_per_h carried by the time-series proto (per-instance values, with a
|
|
446
|
+
# top-level constant fallback resolved by the converter).
|
|
447
|
+
steam_demand_from_proto = (
|
|
448
|
+
df["boiler_steam_demand_kg_per_h"].values
|
|
449
|
+
if "boiler_steam_demand_kg_per_h" in df.columns
|
|
450
|
+
else None
|
|
451
|
+
)
|
|
452
|
+
effective_steam_demand = (
|
|
453
|
+
steam_demand_kg_per_h if steam_demand_kg_per_h is not None else steam_demand_from_proto
|
|
454
|
+
)
|
|
455
|
+
self._set_boiler_steam_demand(effective_steam_demand)
|
|
402
456
|
return self._run_simulation(
|
|
403
457
|
fuel_specified_by=fuel_specified_by,
|
|
404
458
|
ignore_power_balance=ignore_power_balance,
|
|
@@ -414,6 +468,7 @@ class MachineryCalculation:
|
|
|
414
468
|
fuel_specified_by: FuelSpecifiedBy = FuelSpecifiedBy.IMO,
|
|
415
469
|
ignore_power_balance: bool = False,
|
|
416
470
|
fuel_option: Optional[FuelOption] = None,
|
|
471
|
+
steam_demand_kg_per_h: Optional[np.ndarray] = None,
|
|
417
472
|
) -> Union[FEEMSResult, FEEMSResultForMachinerySystem]:
|
|
418
473
|
"""
|
|
419
474
|
Calculate the machinery system output from statistics of the propulsion power.
|
|
@@ -428,19 +483,32 @@ class MachineryCalculation:
|
|
|
428
483
|
ignore_power_balance(bool): If True, the power balance calculation will be ignored.
|
|
429
484
|
fuel_option(FuelOption): The fuel option to be used in the simulation. If None, the
|
|
430
485
|
default fuel option in the system will be used.
|
|
486
|
+
steam_demand_kg_per_h: Steam demand for each mode in kg/h. Must have the same length
|
|
487
|
+
as propulsion_power. If None and a boiler is attached, demand is treated as zero.
|
|
431
488
|
|
|
432
489
|
Returns:
|
|
433
490
|
The result of the simulation. FEEMSResult or FEEMSResultForMachinery system.
|
|
434
491
|
"""
|
|
492
|
+
n_steps = len(propulsion_power)
|
|
435
493
|
if not np.isscalar(auxiliary_power_kw):
|
|
436
494
|
assert (
|
|
437
|
-
|
|
495
|
+
n_steps == len(auxiliary_power_kw) or len(auxiliary_power_kw) == 1
|
|
438
496
|
), "The length of the auxiliary power must be 1 or the same as the propulsion power"
|
|
497
|
+
if steam_demand_kg_per_h is not None:
|
|
498
|
+
if len(steam_demand_kg_per_h) != n_steps:
|
|
499
|
+
raise ValueError(
|
|
500
|
+
f"The length of steam_demand_kg_per_h ({len(steam_demand_kg_per_h)}) must match "
|
|
501
|
+
f"propulsion_power ({n_steps})."
|
|
502
|
+
)
|
|
439
503
|
self._set_input_load_time_interval_from_propulsion_power_time_series(
|
|
440
504
|
propulsion_power_time_series=pd.Series(data=propulsion_power, index=frequency),
|
|
441
505
|
auxiliary_load_kw=auxiliary_power_kw,
|
|
442
506
|
time_is_given_as_interval=True,
|
|
443
507
|
)
|
|
508
|
+
# Time is given as intervals here, so there is no last-point truncation: the helper's
|
|
509
|
+
# alignment is a no-op (n_points == n_steps). Routed through the helper anyway to keep a
|
|
510
|
+
# single boiler-steam code path across all calculation methods.
|
|
511
|
+
self._set_boiler_steam_demand(steam_demand_kg_per_h)
|
|
444
512
|
return self._run_simulation(
|
|
445
513
|
fuel_specified_by=fuel_specified_by,
|
|
446
514
|
ignore_power_balance=ignore_power_balance,
|
|
@@ -13,6 +13,7 @@ from feems.components_model.component_mechanical import (
|
|
|
13
13
|
COGAS,
|
|
14
14
|
EngineMultiFuel,
|
|
15
15
|
FuelCharacteristics,
|
|
16
|
+
SteamBoiler,
|
|
16
17
|
)
|
|
17
18
|
from feems.fuel import FuelOrigin, FuelSpecifiedBy, TypeFuel
|
|
18
19
|
from feems.system_model import ElectricPowerSystem, FuelOption
|
|
@@ -20,6 +21,7 @@ from feems.types_for_feems import EngineCycleType, TypeComponent, TypePower
|
|
|
20
21
|
from MachSysS.convert_to_feems import convert_proto_propulsion_system_to_feems
|
|
21
22
|
from MachSysS.gymir_result_pb2 import (
|
|
22
23
|
GymirResult,
|
|
24
|
+
PropulsionPowerInstance,
|
|
23
25
|
PropulsionPowerInstanceForMultiplePropulsors,
|
|
24
26
|
SimulationInstance,
|
|
25
27
|
TimeSeriesResult,
|
|
@@ -461,3 +463,209 @@ def test_machinery_calculation_multifuel_coges():
|
|
|
461
463
|
rtol=1e-6,
|
|
462
464
|
), "Explicit LNG option should match default"
|
|
463
465
|
|
|
466
|
+
|
|
467
|
+
|
|
468
|
+
# --- Boiler steam demand carried through TimeSeriesResult ---------------------------------
|
|
469
|
+
#
|
|
470
|
+
# These tests cover boiler_steam_demand_kg_per_h on the TimeSeriesResult proto: the value is
|
|
471
|
+
# read back by the converter and applied to the system boiler, producing boiler fuel/CO2 that
|
|
472
|
+
# matches the equivalent table-mode path (calculate_..._from_propulsion_power_time_series with
|
|
473
|
+
# the same steam_demand_kg_per_h array). Both paths use a timestamp series, so the last point is
|
|
474
|
+
# dropped during integration; the helper that builds the proto and the table array shares the
|
|
475
|
+
# same epochs/steam values, so they stay aligned.
|
|
476
|
+
|
|
477
|
+
_BOILER_EPOCHS = [0, 60, 120, 180]
|
|
478
|
+
_BOILER_PROPULSION_KW = [1500.0, 1600.0, 1700.0, 1800.0]
|
|
479
|
+
_BOILER_AUX_KW = 200.0
|
|
480
|
+
|
|
481
|
+
|
|
482
|
+
def _make_steam_boiler() -> SteamBoiler:
|
|
483
|
+
flat_eff = np.array([[0.25, 0.85], [0.50, 0.85], [0.75, 0.85], [1.00, 0.85]])
|
|
484
|
+
return SteamBoiler(
|
|
485
|
+
name="test boiler",
|
|
486
|
+
rated_steam_production_kg_per_h=10_000.0,
|
|
487
|
+
working_pressure_barg=6.0,
|
|
488
|
+
thermal_efficiency_curve=flat_eff,
|
|
489
|
+
fuel_type=TypeFuel.HFO,
|
|
490
|
+
fuel_origin=FuelOrigin.FOSSIL,
|
|
491
|
+
feed_water_temperature_c=80.0,
|
|
492
|
+
uid="boiler-uid-ts",
|
|
493
|
+
)
|
|
494
|
+
|
|
495
|
+
|
|
496
|
+
def _build_boiler_machinery_calculation() -> MachineryCalculation:
|
|
497
|
+
"""Fresh MachineryCalculation (electric system) with a standalone boiler attached.
|
|
498
|
+
|
|
499
|
+
A fresh instance is required per run because boiler.steam_out_kg_per_h is mutated in place.
|
|
500
|
+
"""
|
|
501
|
+
package_dir = os.path.dirname(os.path.abspath(__file__))
|
|
502
|
+
path = os.path.join(package_dir, "system_proto.mss")
|
|
503
|
+
system_feems = convert_proto_propulsion_system_to_feems(retrieve_machinery_system_from_file(path))
|
|
504
|
+
system_feems.boiler = _make_steam_boiler()
|
|
505
|
+
return MachineryCalculation(feems_system=system_feems)
|
|
506
|
+
|
|
507
|
+
|
|
508
|
+
def _table_mode_boiler_result(steam_demand_kg_per_h: np.ndarray):
|
|
509
|
+
series = pd.Series(index=_BOILER_EPOCHS, data=_BOILER_PROPULSION_KW)
|
|
510
|
+
return _build_boiler_machinery_calculation().calculate_machinery_system_output_from_propulsion_power_time_series(
|
|
511
|
+
propulsion_power=series,
|
|
512
|
+
auxiliary_power_kw=_BOILER_AUX_KW,
|
|
513
|
+
steam_demand_kg_per_h=steam_demand_kg_per_h,
|
|
514
|
+
)
|
|
515
|
+
|
|
516
|
+
|
|
517
|
+
def test_time_series_result_per_instance_boiler_steam_matches_table_mode():
|
|
518
|
+
"""AC1: per-timestep boiler_steam_demand_kg_per_h matches the table-mode path."""
|
|
519
|
+
steam = np.array([2000.0, 3000.0, 4000.0, 5000.0])
|
|
520
|
+
time_series = TimeSeriesResult(
|
|
521
|
+
propulsion_power_timeseries=[
|
|
522
|
+
PropulsionPowerInstance(
|
|
523
|
+
epoch_s=epoch,
|
|
524
|
+
propulsion_power_kw=prop,
|
|
525
|
+
auxiliary_power_kw=_BOILER_AUX_KW,
|
|
526
|
+
boiler_steam_demand_kg_per_h=s,
|
|
527
|
+
)
|
|
528
|
+
for epoch, prop, s in zip(_BOILER_EPOCHS, _BOILER_PROPULSION_KW, steam)
|
|
529
|
+
],
|
|
530
|
+
)
|
|
531
|
+
res_proto = _build_boiler_machinery_calculation().calculate_machinery_system_output_from_time_series_result(
|
|
532
|
+
time_series=time_series
|
|
533
|
+
)
|
|
534
|
+
res_table = _table_mode_boiler_result(steam)
|
|
535
|
+
|
|
536
|
+
# Boiler actually ran (guards against a silently-zero comparison).
|
|
537
|
+
assert res_proto.fuel_consumption_boiler_total.total_fuel_consumption > 0
|
|
538
|
+
assert res_proto.steam_production_boiler_total_kg > 0
|
|
539
|
+
# Independent absolute check so a bug in the shared steam-alignment helper can't be hidden by
|
|
540
|
+
# the proto==table comparison (both route through it). The last timestamp is dropped, so the
|
|
541
|
+
# integrated steam is [2000, 3000, 4000] kg/h over three 60 s intervals = 9000/3600*60 = 150 kg.
|
|
542
|
+
assert np.isclose(res_proto.steam_production_boiler_total_kg, 150.0)
|
|
543
|
+
assert np.isclose(
|
|
544
|
+
res_proto.fuel_consumption_boiler_total.total_fuel_consumption,
|
|
545
|
+
res_table.fuel_consumption_boiler_total.total_fuel_consumption,
|
|
546
|
+
)
|
|
547
|
+
assert np.isclose(
|
|
548
|
+
res_proto.fuel_consumption_boiler_total.get_total_co2_emissions().tank_to_wake_kg_or_gco2eq_per_gfuel,
|
|
549
|
+
res_table.fuel_consumption_boiler_total.get_total_co2_emissions().tank_to_wake_kg_or_gco2eq_per_gfuel,
|
|
550
|
+
)
|
|
551
|
+
assert np.isclose(
|
|
552
|
+
res_proto.steam_production_boiler_total_kg,
|
|
553
|
+
res_table.steam_production_boiler_total_kg,
|
|
554
|
+
)
|
|
555
|
+
|
|
556
|
+
|
|
557
|
+
def test_time_series_result_constant_boiler_steam_fallback():
|
|
558
|
+
"""AC2: all-zero per-instance values + non-zero top-level constant behaves as a constant."""
|
|
559
|
+
constant = 3500.0
|
|
560
|
+
time_series = TimeSeriesResult(
|
|
561
|
+
propulsion_power_timeseries=[
|
|
562
|
+
PropulsionPowerInstance(
|
|
563
|
+
epoch_s=epoch,
|
|
564
|
+
propulsion_power_kw=prop,
|
|
565
|
+
auxiliary_power_kw=_BOILER_AUX_KW,
|
|
566
|
+
# boiler_steam_demand_kg_per_h left at 0 on every instance
|
|
567
|
+
)
|
|
568
|
+
for epoch, prop in zip(_BOILER_EPOCHS, _BOILER_PROPULSION_KW)
|
|
569
|
+
],
|
|
570
|
+
boiler_steam_demand_kg_per_h=constant,
|
|
571
|
+
)
|
|
572
|
+
res_proto = _build_boiler_machinery_calculation().calculate_machinery_system_output_from_time_series_result(
|
|
573
|
+
time_series=time_series
|
|
574
|
+
)
|
|
575
|
+
res_table = _table_mode_boiler_result(np.full(len(_BOILER_EPOCHS), constant))
|
|
576
|
+
|
|
577
|
+
assert res_proto.fuel_consumption_boiler_total.total_fuel_consumption > 0
|
|
578
|
+
assert np.isclose(
|
|
579
|
+
res_proto.fuel_consumption_boiler_total.total_fuel_consumption,
|
|
580
|
+
res_table.fuel_consumption_boiler_total.total_fuel_consumption,
|
|
581
|
+
)
|
|
582
|
+
assert np.isclose(
|
|
583
|
+
res_proto.steam_production_boiler_total_kg,
|
|
584
|
+
res_table.steam_production_boiler_total_kg,
|
|
585
|
+
)
|
|
586
|
+
|
|
587
|
+
|
|
588
|
+
def test_time_series_result_zero_boiler_steam_no_contribution():
|
|
589
|
+
"""AC3: all fields zero produces zero boiler contribution (no regression)."""
|
|
590
|
+
time_series = TimeSeriesResult(
|
|
591
|
+
propulsion_power_timeseries=[
|
|
592
|
+
PropulsionPowerInstance(
|
|
593
|
+
epoch_s=epoch,
|
|
594
|
+
propulsion_power_kw=prop,
|
|
595
|
+
auxiliary_power_kw=_BOILER_AUX_KW,
|
|
596
|
+
)
|
|
597
|
+
for epoch, prop in zip(_BOILER_EPOCHS, _BOILER_PROPULSION_KW)
|
|
598
|
+
],
|
|
599
|
+
)
|
|
600
|
+
res_proto = _build_boiler_machinery_calculation().calculate_machinery_system_output_from_time_series_result(
|
|
601
|
+
time_series=time_series
|
|
602
|
+
)
|
|
603
|
+
assert res_proto.fuel_consumption_boiler_total.total_fuel_consumption == 0.0
|
|
604
|
+
assert res_proto.steam_production_boiler_total_kg == 0.0
|
|
605
|
+
|
|
606
|
+
|
|
607
|
+
def test_time_series_result_explicit_steam_demand_overrides_proto():
|
|
608
|
+
"""The explicit steam_demand_kg_per_h argument takes precedence over the proto value."""
|
|
609
|
+
explicit = np.array([2000.0, 3000.0, 4000.0, 5000.0])
|
|
610
|
+
# Proto carries a different (constant) value that must be ignored when the arg is given.
|
|
611
|
+
time_series = TimeSeriesResult(
|
|
612
|
+
propulsion_power_timeseries=[
|
|
613
|
+
PropulsionPowerInstance(
|
|
614
|
+
epoch_s=epoch,
|
|
615
|
+
propulsion_power_kw=prop,
|
|
616
|
+
auxiliary_power_kw=_BOILER_AUX_KW,
|
|
617
|
+
)
|
|
618
|
+
for epoch, prop in zip(_BOILER_EPOCHS, _BOILER_PROPULSION_KW)
|
|
619
|
+
],
|
|
620
|
+
boiler_steam_demand_kg_per_h=9999.0,
|
|
621
|
+
)
|
|
622
|
+
res_override = _build_boiler_machinery_calculation().calculate_machinery_system_output_from_time_series_result(
|
|
623
|
+
time_series=time_series,
|
|
624
|
+
steam_demand_kg_per_h=explicit,
|
|
625
|
+
)
|
|
626
|
+
res_table = _table_mode_boiler_result(explicit)
|
|
627
|
+
assert np.isclose(
|
|
628
|
+
res_override.fuel_consumption_boiler_total.total_fuel_consumption,
|
|
629
|
+
res_table.fuel_consumption_boiler_total.total_fuel_consumption,
|
|
630
|
+
)
|
|
631
|
+
|
|
632
|
+
|
|
633
|
+
def test_time_series_result_multiple_propulsors_boiler_steam():
|
|
634
|
+
"""Per-instance boiler steam is carried through the multi-propulsor time-series path.
|
|
635
|
+
|
|
636
|
+
The boiler depends only on steam demand and the integration intervals, not on how propulsion
|
|
637
|
+
power is split across drives, so the boiler fuel/steam must match the single-propulsor
|
|
638
|
+
table-mode result for the same epochs and steam profile.
|
|
639
|
+
"""
|
|
640
|
+
steam = np.array([2000.0, 3000.0, 4000.0, 5000.0])
|
|
641
|
+
machinery_calculation = _build_boiler_machinery_calculation()
|
|
642
|
+
propulsor_names = [drive.name for drive in machinery_calculation.electric_system.propulsion_drives]
|
|
643
|
+
# Split the same total propulsion power equally across the drives.
|
|
644
|
+
per_drive_power = [[prop / len(propulsor_names)] * len(propulsor_names) for prop in _BOILER_PROPULSION_KW]
|
|
645
|
+
time_series = TimeSeriesResultForMultiplePropulsors(
|
|
646
|
+
propulsion_power_timeseries=[
|
|
647
|
+
PropulsionPowerInstanceForMultiplePropulsors(
|
|
648
|
+
epoch_s=epoch,
|
|
649
|
+
propulsion_power_kw=power_row,
|
|
650
|
+
auxiliary_power_kw=_BOILER_AUX_KW,
|
|
651
|
+
boiler_steam_demand_kg_per_h=s,
|
|
652
|
+
)
|
|
653
|
+
for epoch, power_row, s in zip(_BOILER_EPOCHS, per_drive_power, steam)
|
|
654
|
+
],
|
|
655
|
+
propulsor_names=propulsor_names,
|
|
656
|
+
)
|
|
657
|
+
res_proto = machinery_calculation.calculate_machinery_system_output_from_time_series_result(
|
|
658
|
+
time_series=time_series
|
|
659
|
+
)
|
|
660
|
+
res_table = _table_mode_boiler_result(steam)
|
|
661
|
+
|
|
662
|
+
assert res_proto.fuel_consumption_boiler_total.total_fuel_consumption > 0
|
|
663
|
+
assert np.isclose(res_proto.steam_production_boiler_total_kg, 150.0)
|
|
664
|
+
assert np.isclose(
|
|
665
|
+
res_proto.fuel_consumption_boiler_total.total_fuel_consumption,
|
|
666
|
+
res_table.fuel_consumption_boiler_total.total_fuel_consumption,
|
|
667
|
+
)
|
|
668
|
+
assert np.isclose(
|
|
669
|
+
res_proto.steam_production_boiler_total_kg,
|
|
670
|
+
res_table.steam_production_boiler_total_kg,
|
|
671
|
+
)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|