<SYSTEM_TASK:>
Given the following text description, write Python code to implement the functionality described below step by step
<END_TASK>
<USER_TASK:>
Description:
 2-Dimensional Frame Analysis - Version 04
This program performs an elastic analysis of 2-dimensional structural frames.  It has the following features
Step1:  Test Frame
Nodes
Step2:  Supports
Step3:  Members
Step4:  Releases
Step5:  Properties
If the SST module is loadable, member properties may be specified by giving steel shape designations
(such as 'W310x97') in the member properties data.  If the module is not available, you may still give $A$ and
$I_x$ directly (it only tries to lookup the properties if these two are not provided).
Step6:  Node Loads
Step7:  Member Loads
Step8:  Load Combinations
Step9:  Load Iterators
Step10:  Accumulated Cell Data
Step11:  Input Everything
Step12:  Number the DOFs
Step14:  Display Nodes
Step15:  Display Members
Step16:  Display loads
<ASSISTANT_TASK:>
Python Code:
from __future__ import print_function
import salib as sl
sl.import_notebooks()
from Tables import Table
from Nodes import Node
from Members import Member
from LoadSets import LoadSet, LoadCombination
from NodeLoads import makeNodeLoad
from MemberLoads import makeMemberLoad
from collections import OrderedDict, defaultdict
import numpy as np
class Object(object):
    pass
class Frame2D(object):
    
    def __init__(self,dsname=None):
        self.dsname = dsname
        self.rawdata = Object()
        self.nodes = OrderedDict()
        self.members = OrderedDict()
        self.nodeloads = LoadSet()
        self.memberloads = LoadSet()
        self.loadcombinations = LoadCombination()
        #self.dofdesc = []
        #self.nodeloads = defaultdict(list)
        #self.membloads = defaultdict(list)
        self.ndof = 0
        self.nfree = 0
        self.ncons = 0
        self.R = None
        self.D = None
        self.PDF = None    # P-Delta forces
        
    COLUMNS_xxx = [] # list of column names for table 'xxx'
        
    def get_table(self,tablename,extrasok=False,optional=False):
        columns = getattr(self,'COLUMNS_'+tablename)
        t = Table(tablename,columns=columns,optional=optional)
        t.read(optional=optional)
        reqdl= columns
        reqd = set(reqdl)
        prov = set(t.columns)
        if reqd-prov:
            raise Exception('Columns missing {} for table "{}". Required columns are: {}'\
                            .format(list(reqd-prov),tablename,reqdl))
        if not extrasok:
            if prov-reqd:
                raise Exception('Extra columns {} for table "{}". Required columns are: {}'\
                               .format(list(prov-reqd),tablename,reqdl))
        return t
Explanation: 2-Dimensional Frame Analysis - Version 04
This program performs an elastic analysis of 2-dimensional structural frames.  It has the following features:
1. Input is provided by a set of CSV files (and cell-magics exist so you can specifiy the CSV data
in a notebook cell).  See the example below for an, er, example.
1. Handles concentrated forces on nodes, and concentrated forces, concentrated moments, and linearly varying distributed loads applied transversely anywhere along the member (i.e., there is as yet no way to handle longitudinal
load components).
1. It handles fixed, pinned, roller supports and member end moment releases (internal pins).  The former are
handled by assigning free or fixed global degrees of freedom, and the latter are handled by adjusting the 
member stiffness matrix.
1. It has the ability to handle named sets of loads with factored combinations of these.
1. The DOF #'s are assigned by the program, with the fixed DOF #'s assigned after the non-fixed.  The equilibrium
equation is then partitioned for solution.  Among other advantages, this means that support settlement could be
easily added (there is no UI for that, yet).
1. A non-linear analysis can be performed using the P-Delta method (fake shears are computed at column ends due to the vertical load acting through horizontal displacement differences, and these shears are applied as extra loads
to the nodes).
1. A full non-linear (2nd order) elastic analysis will soon be available by forming the equilibrium equations 
on the deformed structure.  This is very easy to add, but it hasn't been done yet.  Shouldn't be too long.
1. There is very little no documentation below, but that will improve, slowly.
End of explanation
%%Table nodes
NODEID,X,Y,Z
A,0.,0.,5000.
B,0,4000,5000
C,8000,4000,5000
D,8000,0,5000
@sl.extend(Frame2D)
class Frame2D:
    
    COLUMNS_nodes = ('NODEID','X','Y')
        
    def install_nodes(self):
        node_table = self.get_table('nodes')
        for ix,r in node_table.data.iterrows():
            if r.NODEID in self.nodes:
                raise Exception('Multiply defined node: {}'.format(r.NODEID))
            n = Node(r.NODEID,r.X,r.Y)
            self.nodes[n.id] = n
        self.rawdata.nodes = node_table
            
    def get_node(self,id):
        try:
            return self.nodes[id]
        except KeyError:
            raise Exception('Node not defined: {}'.format(id))
##test:
f = Frame2D()
##test:
f.install_nodes()
##test:
f.nodes
##test:
f.get_node('C')
Explanation: Test Frame
Nodes
End of explanation
%%Table supports
NODEID,C0,C1,C2
A,FX,FY,MZ
D,FX,FY
def isnan(x):
    if x is None:
        return True
    try:
        return np.isnan(x)
    except TypeError:
        return False
@sl.extend(Frame2D)
class Frame2D:
    
    COLUMNS_supports = ('NODEID','C0','C1','C2')
    
    def install_supports(self):
        table = self.get_table('supports')
        for ix,row in table.data.iterrows():
            node = self.get_node(row.NODEID)
            for c in [row.C0,row.C1,row.C2]:
                if not isnan(c):
                    node.add_constraint(c)
        self.rawdata.supports = table
##test:
f.install_supports()
vars(f.get_node('D'))
Explanation: Supports
End of explanation
%%Table members
MEMBERID,NODEJ,NODEK
AB,A,B
BC,B,C
DC,D,C
@sl.extend(Frame2D)
class Frame2D:
    
    COLUMNS_members = ('MEMBERID','NODEJ','NODEK')
    
    def install_members(self):
        table = self.get_table('members')
        for ix,m in table.data.iterrows():
            if m.MEMBERID in self.members:
                raise Exception('Multiply defined member: {}'.format(m.MEMBERID))
            memb = Member(m.MEMBERID,self.get_node(m.NODEJ),self.get_node(m.NODEK))
            self.members[memb.id] = memb
        self.rawdata.members = table
            
    def get_member(self,id):
        try:
            return self.members[id]
        except KeyError:
            raise Exception('Member not defined: {}'.format(id))
##test:
f.install_members()
f.members
##test:
m = f.get_member('BC')
m.id, m.L, m.dcx, m.dcy
Explanation: Members
End of explanation
%%Table releases
MEMBERID,RELEASE
AB,MZK
@sl.extend(Frame2D)
class Frame2D:
    
    COLUMNS_releases = ('MEMBERID','RELEASE')
    
    def install_releases(self):
        table = self.get_table('releases',optional=True)
        for ix,r in table.data.iterrows():
            memb = self.get_member(r.MEMBERID)
            memb.add_release(r.RELEASE)
        self.rawdata.releases = table
##test:
f.install_releases()
##test:
vars(f.get_member('AB'))
Explanation: Releases
End of explanation
try:
    from sst import SST
    __SST = SST()
    get_section = __SST.section
except ImportError:
    def get_section(dsg,fields):
        raise ValueError('Cannot lookup property SIZE because SST is not available.  SIZE = {}'.format(dsg))
        ##return [1.] * len(fields.split(',')) # in case you want to do it that way
%%Table properties
MEMBERID,SIZE,IX,A
BC,W460x106,,
AB,W310x97,,
DC,,
@sl.extend(Frame2D)
class Frame2D:
    
    COLUMNS_properties = ('MEMBERID','SIZE','IX','A')
    
    def install_properties(self):
        table = self.get_table('properties')
        table = self.fill_properties(table)
        for ix,row in table.data.iterrows():
            memb = self.get_member(row.MEMBERID)
            memb.size = row.SIZE
            memb.Ix = row.IX
            memb.A = row.A
        self.rawdata.properties = table
        
    def fill_properties(self,table):
        data = table.data
        prev = None
        for ix,row in data.iterrows():
            nf = 0
            if type(row.SIZE) in [type(''),type(u'')]:
                if isnan(row.IX) or isnan(row.A):
                    Ix,A = get_section(row.SIZE,'Ix,A')
                    if isnan(row.IX):
                        nf += 1
                        data.loc[ix,'IX'] = Ix
                    if isnan(row.A):
                        nf += 1
                        data.loc[ix,'A'] = A
            elif isnan(row.SIZE):
                data.loc[ix,'SIZE'] = '' if nf == 0 else prev
            prev = data.loc[ix,'SIZE']
        table.data = data.fillna(method='ffill')
        return table
##test:
f.install_properties()
##test:
vars(f.get_member('DC'))
Explanation: Properties
If the SST module is loadable, member properties may be specified by giving steel shape designations
(such as 'W310x97') in the member properties data.  If the module is not available, you may still give $A$ and
$I_x$ directly (it only tries to lookup the properties if these two are not provided).
End of explanation
%%Table node_loads
LOAD,NODEID,DIRN,F
Wind,B,FX,-200000.
@sl.extend(Frame2D)
class Frame2D:
    
    COLUMNS_node_loads = ('LOAD','NODEID','DIRN','F')
    
    def install_node_loads(self):
        table = self.get_table('node_loads')
        dirns = ['FX','FY','FZ']
        for ix,row in table.data.iterrows():
            n = self.get_node(row.NODEID)
            if row.DIRN not in dirns:
                raise ValueError("Invalid node load direction: {} for load {}, node {}; must be one of '{}'"
                                .format(row.DIRN, row.LOAD, row.NODEID, ', '.join(dirns)))
            l = makeNodeLoad({row.DIRN:row.F})
            self.nodeloads.append(row.LOAD,n,l)
        self.rawdata.node_loads = table
##test:
f.install_node_loads()
##test:
for o,l,fact in f.nodeloads.iterloads('Wind'):
    print(o,l,fact,l*fact)
Explanation: Node Loads
End of explanation
%%Table member_loads
LOAD,MEMBERID,TYPE,W1,W2,A,B,C
Live,BC,UDL,-50,,,,
Live,BC,PL,-200000,,5000
@sl.extend(Frame2D)
class Frame2D:
    
    COLUMNS_member_loads = ('LOAD','MEMBERID','TYPE','W1','W2','A','B','C')
    
    def install_member_loads(self):
        table = self.get_table('member_loads')
        for ix,row in table.data.iterrows():
            m = self.get_member(row.MEMBERID)
            l = makeMemberLoad(m.L,row)
            self.memberloads.append(row.LOAD,m,l)
        self.rawdata.member_loads = table
##test:
f.install_member_loads()
##test:
for o,l,fact in f.memberloads.iterloads('Live'):
    print(o.id,l,fact,l.fefs()*fact)
Explanation: Member Loads
End of explanation
%%Table load_combinations
COMBO,LOAD,FACTOR
One,Live,1.5
One,Wind,1.75
@sl.extend(Frame2D)
class Frame2D:
    
    COLUMNS_load_combinations = ('COMBO','LOAD','FACTOR')
    
    def install_load_combinations(self):
        table = self.get_table('load_combinations')
        for ix,row in table.data.iterrows():
            self.loadcombinations.append(row.COMBO,row.LOAD,row.FACTOR)
        self.rawdata.load_combinations = table
##test:
f.install_load_combinations()
##test:
for o,l,fact in f.loadcombinations.iterloads('One',f.nodeloads):
    print(o.id,l,fact)
for o,l,fact in f.loadcombinations.iterloads('One',f.memberloads):
    print(o.id,l,fact,l.fefs()*fact)
Explanation: Load Combinations
End of explanation
@sl.extend(Frame2D)
class Frame2D:
    def iter_nodeloads(self,comboname):
        for o,l,f in self.loadcombinations.iterloads(comboname,self.nodeloads):
            yield o,l,f
    
    def iter_memberloads(self,comboname):
        for o,l,f in self.loadcombinations.iterloads(comboname,self.memberloads):
            yield o,l,f
##test:
for o,l,fact in f.iter_nodeloads('One'):
    print(o.id,l,fact)
for o,l,fact in f.iter_memberloads('One'):
    print(o.id,l,fact)
Explanation: Load Iterators
End of explanation
##test:
Table.CELLDATA
Explanation: Accumulated Cell Data
End of explanation
@sl.extend(Frame2D)
class Frame2D:
    
    def install_all(self):
        self.install_nodes()
        self.install_supports()
        self.install_members()
        self.install_releases()
        self.install_properties()
        self.install_node_loads()
        self.install_member_loads()
        self.install_load_combinations()
f = Frame2D(dsname='frame-6b')
f.install_all()
Explanation: Input Everything
End of explanation
@sl.extend(Frame2D)
class Frame2D:
    
    def number_dofs(self):
        self.ndof = (3*len(self.nodes))
        self.ncons = sum([len(node.constraints) for node in self.nodes.values()])
        self.nfree = self.ndof - self.ncons
        ifree = 0
        icons = self.nfree
        self.dofdesc = [None] * self.ndof
        for node in self.nodes.values():
            for dirn,ix in node.DIRECTIONS.items():
                if dirn in node.constraints:
                    n = icons
                    icons += 1
                else:
                    n = ifree
                    ifree += 1
                node.dofnums[ix] = n
                self.dofdesc[n] = (node,dirn)
##test:
f.number_dofs()
##test:
f.ndof, f.ncons, f.nfree
Explanation: Number the DOFs
End of explanation
def prhead(txt,ul='='):
    Print a heading and underline it.
    print()
    print(txt)
    if ul:
        print(ul*(len(txt)//len(ul)))
    print()
@sl.extend(Frame2D)
class Frame2D:
    def print_nodes(self,precision=0,printdof=False):
        prhead('Nodes:')
        print('Node          X         Y  Constraints  DOF #s')
        print('----      -----     -----  -----------  ------')
        for nid,node in self.nodes.items():
            ct = ','.join(sorted(node.constraints,key=lambda t: Node.DIRECTIONS[t]))
            dt = ','.join([str(x) for x in node.dofnums])
            print('{:<5s}{:>10.{precision}f}{:>10.{precision}f}  {:<11s}  {}'\
                  .format(nid,node.x,node.y,ct,dt,precision=precision))
        if not printdof:
            return
        print()
        print('DOF#   Node  Dirn')
        print('----   ----  ----')
        for i in range(len(self.dofdesc)):
            node,dirn = self.dofdesc[i]
            print('{:>4d}   {:<4s}  {}'.format(i,node.id,dirn))
##test:
f.print_nodes(printdof=True)
Explanation: Display Nodes
End of explanation
@sl.extend(Frame2D)
class Frame2D:
    
    def print_members(self,precision=1):
        prhead('Members:')
        print('Member   Node-J  Node-K    Length       dcx       dcy  Size                Ix           A  Releases')
        print('------   ------  ------    ------   -------   -------  --------      --------       -----  --------')
        for mid,memb in self.members.items():
            nj = memb.nodej
            nk = memb.nodek
            rt = ','.join(sorted(memb.releases,key=lambda t: Member.RELEASES[t]))
            print('{:<7s}  {:<6s}  {:<6s}  {:>8.{precision}f}  {:>8.5f}  {:>8.5f}  {:<10s}  {:>10g}  {:>10g}  {}'\
                  .format(memb.id,nj.id,nk.id,memb.L,memb.dcx,memb.dcy,str(memb.size),memb.Ix,memb.A,rt,precision=precision))
##test:
f.print_members()
Explanation: Display Members
End of explanation
@sl.extend(Frame2D)
class Frame2D:
    
    def print_loads(self,precision=0):
        
        prhead('Node Loads:')
        if self.nodeloads:
            print('Type  Node      FX          FY          MZ')
            print('----  ----  ----------  ----------  ----------')
            for lname,node,load in self.nodeloads:
                print('{:<4s}  {:<4s}  {:>10.{precision}f}  {:>10.{precision}f}  {:>10.{precision}f}'
                      .format(lname,node.id,load.fx,load.fy,load.mz,precision=precision))
        else:
            print(" - - - none - - -")
        prhead('Member Loads:')
        if self.memberloads:
            print('Type  Member  Load')
            print('----  ------  ----------------')
            for lname,memb,load in self.memberloads:
                print("{:<4s}  {:<6s}  {}".format(lname,memb.id,load))
        else:
            print(" - - - none - - -")
        prhead("Load Combinations:")
        if self.loadcombinations:
            print('Combo  Type  Factor')
            print('-----  ----  ------')
            prev = None
            for cname,lname,f in self.loadcombinations:
                cn = ' '*(len(prev)//2)+'"' if cname == prev else cname
                print("{:<5s}  {:<4s}  {:>6.2f}".format(cn,lname,f))
                prev = cname
        else:
            print(" - - - none - - -")
##test:
f.print_loads()
@sl.extend(Frame2D)
class Frame2D:
    
    def print_input(self):
        
        prhead('Frame '+str(self.dsname)+':')
        print()
        print('              # of nodal degrees of freedom:',self.ndof)
        print('  # of constrained nodal degrees of freedom:',self.ncons)
        print('# of unconstrained nodal degrees of freedom:',self.nfree,' (= degree of kinematic indeterminacy)')
        m = len(self.members)
        r = self.ncons
        j = len(self.nodes)
        c = len(self.rawdata.releases)
        print()
        print('                               # of members:',m)
        print('                             # of reactions:',r)
        print('                                 # of nodes:',j)
        print('                            # of conditions:',c)
        print('             degree of static indeterminacy:',(3*m+r)-(3*j+c))
        print('\n')
        self.print_nodes()
        print('\n')
        self.print_members()
        print('\n')
        self.print_loads()
##test:
f.print_input()
Explanation: Display loads
End of explanation
<END_TASK>