Skip to content

Battery API Reference

Battery

Battery(aircraft)

Battery electro-thermal model with safety checks.

Handles: - Voltage model evaluated at cell level - SOC tracking through current integration - Max current enforcement - Pack configuration (series/parallel) - Thermal dynamics

Source code in trunk/PhlyGreen/Systems/Battery/Battery.py
24
25
26
27
28
29
30
31
32
33
def __init__(self, aircraft):
    self.aircraft = aircraft

    self.pack_Vmax = 800
    self._SOC_min = None
    self._it = 0         # integral of current [Ah], used to compute SOC
    self._i = None       # pack current [A]
    self._T = None       # cell temperature [K]
    self._cell_max_current = None
    self.mdot = 0        # cooling mass flow estimate from heat loss model

E0 property

E0

Voltage constant

K property

K

Polarization resistance (Arrhenius temperature correction)

Q property

Q

Nominal capacity

R property

R

Internal ohmic resistance (Arrhenius temperature correction)

SOC property

SOC

Real-time State-Of-Charge computed from integrated current.

SOC_min property writable

SOC_min

Ensures SOC_min is between 0 and 1.

Voc property

Voc

Open-circuit pack voltage.

Vout property

Vout

Pack-level voltage from series connection.

cell_Voc property

cell_Voc

Open-circuit voltage at the current SOC.

cell_Vout property

cell_Vout

Actual instantaneous cell voltage. Should never drop below the minimum safe voltage.

cell_i property

cell_i

Cell-level current (A). For parallel strings, each cell sees pack current divided by P.

cell_it property

cell_it

Discharge per cell (Ah).

Configure

Configure(parallel_cells)

Defines the parallel count (P_number) and computes pack-level properties: mass, volume, max power, nominal energy.

Parameters

parallel_cells : int Number of cells in parallel (P_number).

Source code in trunk/PhlyGreen/Systems/Battery/Battery.py
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
def Configure(self, parallel_cells):
    """
    Defines the parallel count (P_number) and computes
    pack-level properties: mass, volume, max power, nominal energy.

    Parameters
    ----------
    parallel_cells : int
        Number of cells in parallel (P_number).

    """
    self.P_number = parallel_cells
    self.cells_total = self.P_number * self.S_number

    # physical characteristics of the whole pack:
    stack_length = self.cell_radius * np.ceil(self.S_number / 2)
    # stack_width = self.cell_radius * (2 + np.sqrt(3))
    stack_width = self.cell_radius * 2
    self.pack_volume = self.cell_height * stack_width * stack_length
    self.pack_weight = self.cell_mass * self.cells_total
    if self.pack_weight > 3000000000000000:
        raise BatteryError("OVERWEIGHT! SOMETHING IS VERY WRONG BECAUSE BATTERY IS NOT CONVERGING")
    # nominal pack values
    self.pack_energy = self.cell_energy_nom * self.cells_total
    self.pack_power_max = (
        self.cell_max_current * self._voltageModel(0, self.cell_max_current) * self.cells_total
    )
    self.pack_charge = self.cell_capacity * self.P_number

Power_2_current

Power_2_current(P)

Calculates the current output from the battery. The calculations are for a single cell, as that is what the model is made for. The output is the current for the entire battery pack however. The power is simply divided by the total number of cells, as every cell delivers equal power regardless of the configuration of the battery. Receives: P - power demanded from the battery Returns: I_out - current output from the battery

Source code in trunk/PhlyGreen/Systems/Battery/Battery.py
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
def Power_2_current(self, P):
    """Calculates the current output from the battery. The calculations are for a single
        cell, as that is what the model is made for. The output is the current for the entire
        battery pack however. The power is simply divided by the total number of cells, as
        every cell delivers equal power regardless of the configuration of the battery.
    Receives:
        P - power demanded from the battery
    Returns:
        I_out - current output from the battery
    """

    if P == 0:  # skips all the math if power is zero
        return 0

    # V = E0 - I*R - I*K*(Q/(Q-it)) - it*K*(Q/(Q-it)) + A*exp(-B * it)
    # V = E0 - I*R - I*Qr - it*Qr + ee <- with substitutions to make shorter
    # P = V*I = E0*I - I^2*R - I^2*Qr - I*it*Qr + I*ee 
    # P = I^2 *(-R-Qr) + I *(E0+ee-it*Qr)
    # quadratic solve: 
    # a*I^2 + b*I - P = 0

    E0, R, K, Q = self.E0, self.R, self.K, self.Q
    A, B = self.exp_amplitude, self.exp_time_ctt
    it = self.cell_it
    P = P / self.cells_total  # all cells deliver the same power

    Qr = K * Q / (Q - it)
    ee = A * np.exp(-B * it)
    a = -R - Qr
    b = E0 + ee - it * Qr
    c = -P
    Disc = b**2 - 4 * a * c  # quadratic formula discriminant

    if Disc < 0:
        I_out = None
        return I_out

    else:
        I_out = (-b + np.sqrt(Disc)) / (2 * a)  # just the quadratic formula

    return I_out * self.P_number

SetInput

SetInput()

This grabs the CellModel object from the aircraft class where a dictionary defines some inputs set by the user. Then according to the inputs it defines the battery model to use. The constants come from the cell_models.py module and are modified according to the user input.

Source code in trunk/PhlyGreen/Systems/Battery/Battery.py
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
def SetInput(self):
    """
    This grabs the CellModel object from the aircraft class where a
    dictionary defines some inputs set by the user.
    Then according to the inputs it defines the battery model to use.
    The constants come from the cell_models.py module and are modified
    according to the user input.
    """

    bat_inputs = self.aircraft.CellInput
            # set the battery model class
    self.BatteryClass = bat_inputs['Class']


    if self.BatteryClass == 'II':
        self.SOC_min = bat_inputs['Minimum SOC']
        self.aircraft.mission.startT = bat_inputs['Initial temperature'] 
        self.aircraft.mission.T_battery_limit = bat_inputs['Max operative temperature'] 

    if self.BatteryClass == 'I':
        self.Ebat = bat_inputs['SpecificEnergy']*3600
        self.pbat = bat_inputs['SpecificPower']
        self.SOC_min = bat_inputs['Minimum SOC'] 
        return

    if self.BatteryClass != 'II':
        raise Exception(f"Unrecognized model class: {self.BatteryClass}")

    if bat_inputs['Model'] is None: # Fallback to a default model if none is given
        model = 'Finger-Cell-Thermal'
    else:
        model = bat_inputs['Model']



    # Get all the cell parameters
    cell = Cell_Models[model]
    self.Tref              = cell['Reference Temperature']     # in kelvin
    self.T = self.Tref
    self.exp_amplitude     = cell['Exp Amplitude']                  # in volts
    self.exp_time_ctt      = cell['Exp Time constant']              # in Ah^-1 
    self.cell_resistance   = cell['Internal Resistance']            # in ohms
    self.R_arrhenius       = cell['Resistance Arrhenius Constant']  # dimensionless
    self.polarization_ctt  = cell['Polarization Constant']          # in Volts over amp hour
    self.K_arrhenius       = cell['Polarization Arrhenius Constant']# dimensionless
    self.cell_capacity     = cell['Cell Capacity']                  # in Ah
    self.Q_slope           = cell['Capacity Thermal Slope']         # in Ah per kelvin
    self.voltage_ctt       = cell['Voltage Constant']               # in volts
    self.E_slope           = cell['Voltage Thermal Slope']          # in volts per kelvin
    self.cell_Vmax         = self.exp_amplitude + self.voltage_ctt  # in volts
    self.cell_Vmin         = cell['Cell Voltage Min']               # in volts
    self.cell_max_current  = cell['Cell Current Max']                  # dimensionless
    self.cell_mass         = cell['Cell Mass']                      # in kg
    self.cell_radius       = cell['Cell Radius']                    # in m
    self.cell_height       = cell['Cell Height']                    # in m
    self.Vnom              = cell['Cell Voltage Nominal']  # in volts
    self.cell_energy_nom   = self.Vnom * self.cell_capacity

    # Verify that the voltages are correctly set
    if not (self.cell_Vmax > self.cell_Vmin):
        raise ValueError(
            "Fail_Condition_11\nIllegal cell voltages: Vmax must be greater than Vmin"
        )

    # Modify the cell according to the user specified energy and power densities
    # Modify the capacity of the cell
    if bat_inputs["SpecificEnergy"] is not None:
        ecell = bat_inputs["SpecificEnergy"] * self.cell_mass   # cell energy in Wh
        capcell = ecell / self.Vnom                             # cell charge in Ah
        eratio = capcell / self.cell_capacity # ratio between model charge and new charge
        #print(f"old{self.cell_capacity}    new{capcell}")

        self.cell_capacity = capcell
        self.cell_energy_nom = ecell
        self.Q_slope *= eratio  # the slope is a fraction of the capacity, so it scales with the ratio of capacities
        self.exp_time_ctt /= eratio  # divides by the ratio because its the INVERSE of the exponential zone charge

        # Scale the specific power accordingly, unless a fixed one is requested
        if bat_inputs["SpecificPower"] is None:
            self.polarization_ctt /= eratio
            self.K_arrhenius /= eratio
            self.cell_resistance /= eratio
            self.R_arrhenius /= eratio
            self.cell_max_current *= eratio

        # implement this properly later if needed
        # make the specific power a ratio of the specific energy
        # to be able to pick a certain C rating
        # if bat_inputs["SpecificPower"] is None:
        #     pcell = 4 * bat_inputs["SpecificEnergy"] * self.cell_mass  # cell power in W
        #     pcellnow = self.cell_max_current * self._voltageModel(0, self.cell_max_current)
        #     pratio = pcell / pcellnow

        #     self.polarization_ctt /= pratio
        #     self.K_arrhenius /= pratio
        #     self.cell_resistance /= pratio
        #     self.R_arrhenius /= pratio
        #     self.cell_max_current *= pratio


    # Modify the internal resistance and current limit to adjust power
    if bat_inputs["SpecificPower"] is not None:
        pcell = bat_inputs["SpecificPower"] * self.cell_mass  # cell power in W
        pcellnow = self.cell_max_current * self._voltageModel(0, self.cell_max_current)
        pratio = pcell / pcellnow
        #print(f"Current ratio:{pratio}")
        #print(f"peak power old:{pcellnow}")

        # Dividing the internal resistance by a ratio increases
        # the maximum deliverable power by the same ratio
        self.polarization_ctt /= pratio
        self.K_arrhenius /= pratio
        self.cell_resistance /= pratio
        self.R_arrhenius /= pratio

        # Note: the cell max current needs to be the last thing
        # to be calculated because its setter verifies its
        # validity against the cell properties
        self.cell_max_current *= pratio
    # print(f"peak power new:{self.cell_max_current*self._voltageModel(0, self.cell_max_current)}")


    # Number of cells in series to achieve desired voltage.
    # Higher voltage is preferred as it minimizes losses
    # due to lower current being needed.
    if bat_inputs["Pack Voltage"] is not None:
        self.pack_Vmax = bat_inputs["Pack Voltage"]
    else:
        self.pack_Vmax = 740
    self.S_number = np.floor(self.pack_Vmax / self.cell_Vmax)

    self.cell_area_surface = 2*np.pi*self.cell_radius*self.cell_height
    self.module_area_section = (2*self.cell_radius)**2-np.pi*self.cell_radius**2

    self.Rith = 3.3*(self.cell_radius/0.022)**2 # probably need a citation for this one
    self.Cth = 1000 * self.cell_mass

heatLoss

heatLoss(Ta, rho)

Simple differential equation describing a simplified lumped element thermal model of the cells Receives: - Ta - temperature of the ambient cooling air - rho - density of the ambient air Returns: - dTdt - battery temperature derivative - P - dissipated waste power per cell

Source code in trunk/PhlyGreen/Systems/Battery/Battery.py
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
def heatLoss(self, Ta, rho):
    """ Simple differential equation describing a
        simplified lumped element thermal model of the cells
    Receives:
        - Ta   - temperature of the ambient cooling air
        - rho  - density of the ambient air
    Returns:
        - dTdt - battery temperature derivative
        - P    - dissipated waste power per cell
    """
    Ta = max(Ta,273.15)
    # print('T ambient: ', Ta)

    V, Voc = self.cell_Vout, self.cell_Voc
    i = self.cell_i
    T, dEdT = self.T, self.E_slope
    # print('Battery temperature: ', T-273.15)
    Rith = self.Rith
    Cth = self.Cth
    P = (Voc - V) * i + dEdT * i * T
    self.mdot = 0.0001*P
    # if P<0:
    #     self.mdot = 0
    h = max(
        (  # taken from http://dx.doi.org/10.1016/j.jpowsour.2013.10.052
            30* ( ((self.mdot) / (self.module_area_section * rho)) / 5) ** 0.8
        ),
        2)

    if (self.phi > 0.) & (T < 273.15 + self.aircraft.mission.T_battery_limit):
        h = 0.            


    # print(self.mdot)

    if h == 0:  # avoid division by 0
        dTdt = P / Cth
    else:
        Rth = 1 / (h * self.cell_area_surface) + Rith
        dTdt = P / Cth + (Ta - T) / (Rth * Cth)
    # print(f"h: {h}   R:{Rth}     surface:{self.cell_area_surface}    crosssec:{self.module_area_section}")
    return dTdt, P

BatteryError

BatteryError(message, code=None)

Bases: Exception

Custom exception to be caught when the battery violates physical or model constraints.

Source code in trunk/PhlyGreen/Systems/Battery/Battery.py
 8
 9
10
def __init__(self, message, code=None):
    super().__init__(message)
    self.code = code