<jupyter_start><jupyter_text>PyIndMach012: an example of user-model using DSS PythonThis example runs a modified example from the OpenDSS distribution for the induction machine model with a sample PyIndMach012 implementation, written in Python, and the original, built-in IndMach012.Check the `PyIndMach012.py` file for more comments. Comparing it to [the Pascal code for IndMach012](https://github.com/dss-extensions/dss_capi/blob/master/Version7/Source/PCElements/IndMach012.pas) can be useful to understand some of the inner workings of OpenDSS.The user-model code in DSS Python is not stable yet but can be used to develop new ideas before commiting the final model in a traditional DLL user-model. Particularly, I (@PMeira) found some issues with callbacks with newer Version 8 COM DLLs, so changes related to that are expected.<jupyter_code>%matplotlib inline
import os
import numpy as np
from matplotlib import pyplot as plt
from dss.UserModels import GenUserModel # used to get the DLL path
import PyIndMach012 # we need to import the model so it gets registered<jupyter_output><empty_output><jupyter_text>The model class<jupyter_code>??PyIndMach012<jupyter_output><empty_output><jupyter_text>OpenDSS setup For this example, we can use either COM or DSS Python (DSS C-API). The IndMach012 model in DSS C-API seems to have a bug somewhere though -- this is being tracked in [dss_capi62](https://github.com/dss-extensions/dss_capi/issues/62).<jupyter_code>original_dir = os.getcwd() # same the original working directory since the COM module messes with it
USE_COM = True # toggle this value to run with DSS C-API
if USE_COM:
    from dss import patch_dss_com
    import win32com.client
    DSS = patch_dss_com(win32com.client.gencache.EnsureDispatch('OpenDSSengine.DSS'))
    DSS.DataPath = original_dir
    os.chdir(original_dir)
else:
    from dss import DSS
    
DSS.Version    
Text = DSS.Text
Monitors = DSS.ActiveCircuit.Monitors<jupyter_output><empty_output><jupyter_text>Using the modelTo use a Python model for generators:- the model class needs to be registered in advance- create a generator with `model=6`    - pass a `usermodel="{dll_path}"` as in the following DSS command in the `run` function    - pass a `"pymodel=MODELNAME"` parameter in the userdata property, where MODELNAME is the name of the model class in Python<jupyter_code>def run(pymodel):
    Text.Command = 'redirect "master.dss"'

    if pymodel:
        # This uses our custom user-model in Python
        Text.Command = 'New "Generator.Motor1" bus1=Bg2 kW=1200 conn=delta kVA=1500.000 H=6 model=6 kv=0.48 usermodel="{dll_path}" userdata=(pymodel=PyIndMach012 purs=0.048 puxs=0.075 purr=0.018 puxr=0.12 puxm=3.8 slip=0.02 SlipOption=variableslip)'.format(
            dll_path=GenUserModel.dll_path,
        )
        Text.Command = 'New "Monitor.mfr2" element=Generator.Motor1 terminal=1 mode=3'
    else:
        # This uses the built-in model for comparison
        Text.Command = 'New "IndMach012.Motor1" bus1=Bg2 kW=1200 conn=delta kVA=1500.000 H=6 purs=0.048 puxs=0.075 purr=0.018 puxr=0.12 puxm=3.8 slip=0.02 SlipOption=variableslip kv=0.48'
        Text.Command = 'New "Monitor.mfr2" element=IndMach012.Motor1 terminal=1 mode=3'
        
    # This will run a power-flow solution
    Text.Command = 'Solve'
    
    # This will toggle to the dynamics mode
    Text.Command = 'Set mode=dynamics number=1 h=0.000166667'
    
    # And finally run 5000 steps for the dynamic simulation
    Text.Command = f'Solve number=5000'
   
# There are the channels from the Pascal/built-in IndMach012
channels_pas = (' Frequency', 'Theta (deg)', 'E1', 'dSpeed (deg/sec)', 'dTheta (deg)', 'Slip', 'Is1', 'Is2', 'Ir1', 'Ir2', 'Stator Losses', 'Rotor Losses', 'Shaft Power (hp)', 'Power Factor', 'Efficiency (%)')

# There are the channels from the Python module -- we define part of these and part come from the generator model itself
channels_py = (' Frequency', 'Theta (Deg)', 'E1_pu', 'dSpeed (Deg/sec)', 'dTheta (Deg)', 'Slip', 'Is1', 'Is2', 'Ir1', 'Ir2', 'StatorLosses', 'RotorLosses', 'ShaftPower_hp', 'PowerFactor', 'Efficiency_pct')<jupyter_output><empty_output><jupyter_text>Running and saving the outputs Let's run the Pascal/built-in version of IndMach012 and our custom Python version for comparison:<jupyter_code>run(False)
Monitors.Name = 'mfr2'
outputs_pas = {channel: Monitors.Channel(Monitors.Header.index(channel) + 1) for channel in channels_pas}

run(True)
Monitors.Name = 'mfr2'
outputs_py = {channel: Monitors.Channel(Monitors.Header.index(channel) + 1) for channel in channels_py}

time = np.arange(1, 5000 + 1) * 0.000166667
offset = int(0.1 / 0.000166667)<jupyter_output><empty_output><jupyter_text>Plotting the various output channels The example circuit applies a fault at 0.3 s, isolating the machine at 0.4s (check `master.dss` for more details).As we can see from the figures below, the outputs match very closely. After the induction machine is isolated, the efficiency and power factor values can misbehave as the power goes to zero, seem especially in the Pascal version.<jupyter_code>for ch_pas, ch_py in zip(channels_pas, channels_py):
    plt.figure(figsize=(8,4))
    plt.plot(time, outputs_pas[ch_pas], label='IndMach012', lw=3)
    plt.plot(time, outputs_py[ch_py], label='PyIndMach012', ls='--', lw=2)
    plt.axvline(0.3, linestyle=':', color='k', alpha=0.5, label='Fault occurs')
    plt.axvline(0.4, linestyle='--', color='r', alpha=0.5, label='Relays operate')
    plt.legend()
    plt.xlabel('Time (s)')
    plt.ylabel(ch_pas)
    
    if ch_pas == 'Efficiency (%)':
        # Limit efficiency to 0-100
        plt.ylim(0, 100)
        
    plt.xlim(0, time[-1])
    plt.tight_layout()<jupyter_output><empty_output>
{'fraction_non_alphanumeric': 0.10642745166347327, 'fraction_numerical': 0.040236892527434245, 'mean_word_length': 4.542471042471043, 'pattern_counts': {'":': 0, '<': 25, '<?xml version=': 0, '>': 25, 'https://': 2, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'chain_length': '6', 'dir': 'jupyter-structured-clean-dedup', 'id': '6279', 'license': 'BSD-3-Clause', 'n_tokens_mistral': 2056, 'n_tokens_neox': 1881, 'n_words': 603, 'path': 'docs/examples/UserModels/PyIndMach012/README.ipynb', 'repo_name': 'dss-extensions/dss_python', 'size': '5741'}