├── .gitignore ├── +lsdyna ├── Contents.m ├── +read │ ├── kfile.m │ ├── nodfor.m │ ├── bndout.m │ ├── asciiFiles.m │ ├── nodout.m │ ├── rbdout.m │ ├── DATABASE_FILE.m │ └── elout.m ├── +keyword │ ├── PART.m │ ├── NODE.m │ ├── card_base.m │ ├── utils.m │ ├── ELEMENT_BEAM.m │ ├── ELEMENT_DISCRETE.m │ ├── ELEMENT_SHELL.m │ ├── card.m │ ├── ELEMENT_SOLID.m │ └── file.m └── simulation.m └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | +/.ignore 2 | +*.txt 3 | +*.asv 4 | +*~ 5 | +*.mex* -------------------------------------------------------------------------------- /+lsdyna/Contents.m: -------------------------------------------------------------------------------- 1 | % Commands for running and reading LS-Dyna simulations from MATLAB 2 | % MATLAB Version 9.0 (R2016a) 05-May-2016 3 | % 4 | % Creating and running simulations 5 | % simulation - Make/read an LS-Dyna simulation from a folder 6 | % 7 | % Reading ASCII LS-Dyna output database files 8 | % read.asciiFiles - Read all available output databases 9 | % read.bndout - Read boundary conditions output 10 | % read.elout - Read element data output 11 | % read.nodfor - Read nodal forces data output 12 | % read.nodout - Read nodal coord/disp/vel/acc data output 13 | % read.rbdout - Read rigid body data output 14 | 15 | % Not implemented yet 16 | % These functions are planned. 17 | % 18 | % LS-Dyna input deck creation 19 | % card.NODE - Create *NODE deck from given xyz coordinates 20 | % card.ELEMENT - Create *ELEMENT deck from faces/vertices 21 | % ... 22 | 23 | % Sven Holcombe -------------------------------------------------------------------------------- /+lsdyna/+read/kfile.m: -------------------------------------------------------------------------------- 1 | function [PART, NODE, ELEMENT_SHELL, ELEMENT_SOLID, ELEMENT_SHELL_THICKNESS] = kfile(kFileStr) 2 | % UNDER CONSTRUCTION! This file contains some basic logic to parse a dyna 3 | % k-file but the overall API for reading and storing that information is 4 | % not complete. 5 | 6 | % kFileStr = 'GHBMC_M50-O_v4-5_20160901.k'; 7 | 8 | % Read the kfile and extract separate cards 9 | if isa(kFileStr,'lsdyna.keyword.file') 10 | F = kFileStr; 11 | else 12 | F = lsdyna.keyword.file.readKfile(kFileStr); 13 | end 14 | [~, ~, ~, ~, ELEMENT_SHELL_THICKNESS] = deal([]); 15 | %% 16 | C_NODE = F.Cards(startsWith([F.Cards.Keyword],"NODE")); 17 | NODE = cat(1,C_NODE.NodeData); 18 | C_PART = F.Cards(startsWith([F.Cards.Keyword],"PART")); 19 | PART = table([C_PART.Heading]',uint32([C_PART.PID]'),uint32([C_PART.SID]'),uint32([C_PART.MID]'),'Var',{ 20 | 'heading' 'pid' 'secid' 'mid'}); 21 | C_ELEM = F.Cards(startsWith([F.Cards.Keyword],"ELEMENT_SHELL_THICKNESS")); 22 | if ~isempty(C_ELEM) 23 | ELEMENT_SHELL_THICKNESS = cat(1,C_ELEM.ElemData); 24 | end 25 | C_ELEM = F.Cards([F.Cards.Keyword]=="ELEMENT_SHELL"); 26 | ELEMENT_SHELL = cat(1,C_ELEM.ElemData); 27 | C_ELEM = F.Cards(startsWith([F.Cards.Keyword],"ELEMENT_SOLID")); 28 | ELEMENT_SOLID = cat(1,C_ELEM.ElemData); 29 | 30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /+lsdyna/+keyword/PART.m: -------------------------------------------------------------------------------- 1 | classdef PART < lsdyna.keyword.card 2 | %lsdyna.keyword.PART 3 | 4 | properties (Constant) 5 | KeywordMatch = "PART"; 6 | end 7 | properties (Constant, Hidden) 8 | DependentCards = ["MAT" "SECTION"] 9 | end 10 | 11 | properties 12 | Heading(1,1) string = "" 13 | PID (1,1) double = nan 14 | MID (1,1) double = nan 15 | SID (1,1) double = nan 16 | end 17 | 18 | %% CONSTRUCTOR 19 | methods 20 | function newCard = PART(basicCard) 21 | % Allow empty constructor 22 | if ~nargin 23 | return; 24 | end 25 | % Elseif isa basicCard then convert 26 | newCard = basicCard.assignPropsToSubclass(newCard); 27 | % Else call superclass constructor on varargin 28 | end 29 | 30 | function C = arr_stringToData(C) 31 | % Parse the string data and populate this card's numeric data 32 | 33 | activeStrs = {C.ActiveString}; 34 | % The heading will always be the first line 35 | headings = cellstr(deblank(cellfun(@(x)x(1),activeStrs))); 36 | [C.Heading] = headings{:}; 37 | 38 | % PID, MID, SID are all in the second line 39 | line2s = cellfun(@(x)x(2),activeStrs(:)); 40 | PIDs = num2cell(double(extractBefore(line2s,11))); 41 | [C.PID] = PIDs{:}; 42 | MIDs = num2cell(double(extractBetween(line2s,11, 20))); 43 | [C.MID] = MIDs{:}; 44 | SIDs = num2cell(double(extractBetween(line2s,21, 30))); 45 | [C.SID] = SIDs{:}; 46 | end 47 | end 48 | end -------------------------------------------------------------------------------- /+lsdyna/+read/nodfor.m: -------------------------------------------------------------------------------- 1 | classdef nodfor < lsdyna.read.DATABASE_FILE 2 | %NODFOR Read a nodal forces output (nodfor) LS-DYNA ascii file 3 | % nodfor = lsdyna.read.nodfor(folder) 4 | 5 | properties 6 | file = 'nodfor' 7 | NODE_INFO 8 | NODE_DATA 9 | end 10 | methods 11 | function this = nodfor(varargin) 12 | this = this@lsdyna.read.DATABASE_FILE(varargin{:}); 13 | end 14 | end 15 | methods (Hidden) 16 | 17 | function addDerivedDataChannels(~) 18 | end 19 | function parseFileContents(this, inStr) 20 | 21 | %% 22 | % Regular expression matching any (optionally) scientific notation number 23 | sciFloatPattern = '[-+]?[0-9]*\.?[0-9]+([eE][-+]?[0-9]+)?'; 24 | 25 | % Get all instances of "var= value" throughout file 26 | [a,~,~,~,e] = regexp(inStr,['(\w+)=\s+(' sciFloatPattern ')']); 27 | e = cat(1,e{:}); 28 | % Extract the time sequence, given only once per timestep 29 | tmask = strcmp('t',e(:,1)); 30 | etVals = str2num(char(e(:,2))); %#ok 31 | eVals = etVals(~tmask); 32 | tVals = etVals(tmask); 33 | tNos = cumsum(tmask); 34 | tNos = tNos(~tmask); 35 | eLabs = e(~tmask,1); 36 | [unqLabs, ~, eLabGrp] = unique(eLabs,'stable'); 37 | 38 | nTimesteps = nnz(tmask); 39 | nVars = length(unqLabs); 40 | nNodes = length(eVals) / nTimesteps / nVars; 41 | nodeNos = reshape(repmat(1:nNodes, nVars, nTimesteps),[],1); 42 | dataMat = zeros(nTimesteps, nVars, nNodes); 43 | 44 | dataMat(sub2ind(size(dataMat), tNos, eLabGrp, nodeNos)) = eVals; 45 | 46 | % Get the nodeId (nd# 12345) strings giving N nodes 47 | [~,~,~,~,nodNoStrs] = regexp(inStr(a(1):a(nVars*nNodes)),'nd#\s+(\d+)'); 48 | nodeIds = cellfun(@str2double,cat(2,nodNoStrs{:})); 49 | 50 | this.NODE_INFO = array2table(nodeIds(:),'Var',{'NODE_ID'}); 51 | this.NODE_DATA = array2table(tVals,'Var',{'timestep'}); 52 | for vn = 1:length(unqLabs) 53 | this.NODE_DATA.(unqLabs{vn}) = permute(dataMat(:,vn,:),[1 3 2]); 54 | end 55 | end 56 | end 57 | end -------------------------------------------------------------------------------- /+lsdyna/+keyword/NODE.m: -------------------------------------------------------------------------------- 1 | classdef NODE < lsdyna.keyword.card 2 | %lsdyna.keyword.NODE 3 | 4 | properties (Constant) 5 | KeywordMatch = "NODE"; 6 | LineDefinitions = lsdyna.keyword.utils.cardLineDefinition("NODE"); 7 | end 8 | properties (Constant, Hidden) 9 | DependentCards = strings(1,0) 10 | end 11 | 12 | properties 13 | NodeData = table; 14 | end 15 | 16 | methods 17 | function strs = sca_dataToString(C) 18 | FLDS = C.LineDefinitions.FLDS{1}; 19 | DATA = table2cell(C.NodeData)'; 20 | 21 | printSpec = strjoin("%" + FLDS.size + FLDS.fmt,"") + newline; 22 | strs = splitlines(sprintf(printSpec, DATA{:})); 23 | strs = strs(1:end-1); 24 | end 25 | end 26 | 27 | %% CONSTRUCTOR 28 | methods 29 | function newCard = NODE(basicCard) 30 | % Allow empty constructor 31 | if ~nargin 32 | return; 33 | end 34 | % Elseif isa basicCard then convert 35 | newCard = basicCard.assignPropsToSubclass(newCard); 36 | % Else call superclass constructor on varargin 37 | end 38 | 39 | function C = arr_stringToData(C) 40 | % Parse the string data and populate this card's numeric data 41 | lineDefns = C(1).LineDefinitions; 42 | % There's only 1 repeated line for NODE cards. Just use it. 43 | FLDS = lineDefns.FLDS{1}; 44 | 45 | % Grab and concatenate each card's active strings 46 | strs = cat(1,C.ActiveString); 47 | strsLineCounts = cellfun(@numel,{C.ActiveString}); 48 | 49 | % Turn comma-sep lines into spaced lines and read the data 50 | strs = C.convertCommaSepStrsToSpacedStrs(strs,FLDS.size); 51 | NODEDATA = C.convertSpacedStrsToMatrix(strs,FLDS); 52 | 53 | % Convert appropriate data field types 54 | NODE = array2table(NODEDATA,'Var',FLDS.fld); 55 | NODE.nid = uint32(NODE.nid); 56 | NODE.tc = uint8(NODE.tc); 57 | NODE.rc = uint8(NODE.rc); 58 | 59 | % Identify rows belonging to each individual card 60 | linesEndAt = cumsum(strsLineCounts); 61 | linesStartAt = [1 linesEndAt(1:end-1)+1]; 62 | NODEset = arrayfun(@(from,to)NODE(from:to,:),linesStartAt,linesEndAt,'Un',0); 63 | [C.NodeData] = NODEset{:}; 64 | end 65 | end 66 | end -------------------------------------------------------------------------------- /+lsdyna/+read/bndout.m: -------------------------------------------------------------------------------- 1 | classdef bndout < lsdyna.read.DATABASE_FILE 2 | %BNDOUT Read a boundary output (bndout) LS-DYNA ascii file 3 | % bndout = lsdyna.read.rbdout(bndout) 4 | 5 | properties 6 | file = 'bndout' 7 | BND_INFO 8 | BND_DATA 9 | end 10 | methods 11 | function this = bndout(varargin) 12 | this = this@lsdyna.read.DATABASE_FILE(varargin{:}); 13 | end 14 | end 15 | methods (Hidden) 16 | 17 | function addDerivedDataChannels(~) 18 | end 19 | function parseFileContents(this, inStr) 20 | 21 | %% 22 | % Regular expression matching any (optionally) scientific notation number 23 | sciFloatPattern = '[-+]?[0-9]*\.?[0-9]+([eE][-+]?[0-9]+)?'; 24 | % Find the header (timing) lines 25 | tStepPattern = ['^ n o d a l f o r c e/e n e r g y o u t p u t t=\s*(' sciFloatPattern ')']; 26 | [~,~,~,~,te] = regexp(inStr,tStepPattern,'lineanchors'); 27 | te = cat(1,te{:}); 28 | timestepArr = str2num(char(te)); %#ok 29 | nTimesteps = length(timestepArr); 30 | 31 | if ~nTimesteps 32 | return 33 | end 34 | %% 35 | [~,matStrEnds,~,~,matNosTxt] = regexp(inStr,'^mat#\s*(\d+)','lineanchors'); 36 | matTxt = cat(1,matNosTxt{:}); 37 | matIds = str2num(char(matTxt)); %#ok 38 | [unqMatIds, firstMatId] = unique(matIds); 39 | 40 | % bndout stores some xyz components - let's combine them 41 | triplets = cell2table({ 42 | 'force', [11:23 33:45 55:67] 43 | 'moment', [11:23 33:45 55:67]+122 44 | 'total', [11:23 33:45 55:67]+122+81 45 | },'Var',{'fld','strInds'}); 46 | 47 | inStrInds = bsxfun(@plus, triplets.strInds, reshape(matStrEnds,1,1,[])); 48 | % Get values for force, moment, total in 49 | % timestep-by-xyz-by-mat-by-fmt array 50 | tripletVals = permute(reshape(str2num(inStr(inStrInds(:,:))),... 51 | size(inStrInds,1),3,[], nTimesteps), [4 2 3 1]); %#ok 52 | 53 | this.BND_DATA = array2table(timestepArr,'Var',{'timestep'}); 54 | for i = 1:size(triplets,1) 55 | this.BND_DATA.([triplets.fld{i} '_xyz']) = tripletVals(:,:,:,i); 56 | end 57 | % Append the energy output by force and total 58 | this.BND_DATA.energy = reshape(str2num(inStr(... 59 | bsxfun(@plus,matStrEnds(:), 78:90)))',[],nTimesteps)'; %#ok 60 | this.BND_DATA.etotal = reshape(str2num(inStr(... 61 | bsxfun(@plus,matStrEnds(:), (78:90)+203)))',[],nTimesteps)'; %#ok 62 | % Append mat numbers and the set they belong to 63 | this.BND_INFO = array2table(unqMatIds(:), 'Var',{'MAT_ID'}); 64 | this.BND_INFO.SET_ID = str2num(inStr(bsxfun(@plus,matStrEnds(firstMatId)', 101:113))); %#ok 65 | end 66 | end 67 | end -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # matlab-lsdyna 2 | 3 | This project contains a reader of ascii results files from the Finite Element solver LS-DYNA, and a wrapper to run LS-DYNA simulations programmatically from MATLAB. This project is not affiliated in any way with the creators or distributors of LS-DYNA and thus is totally unofficial. 4 | 5 | Currently, matlab-lsdyna is written for and tested on a Windows environment. ASCII database reading should by system independent, but code to run simulations is expected to fail on other systems. Efforts to further the tested environments are welcome. 6 | All code is written in MATLAB by Sven Holcombe. 7 | 8 | # Features 9 | 10 | ## Creating and running simulations 11 | - *lsdyna.simulation* - Make/read an LS-Dyna simulation from a folder 12 | 13 | ## Reading ASCII LS-Dyna output database files 14 | - *lsdyna.read.asciiFiles* - Read all available output databases 15 | - *lsdyna.read.bndout* - Read boundary conditions output 16 | - *lsdyna.read.elout* - Read element data output 17 | - *lsdyna.read.nodfor* - Read nodal forces data output 18 | - *lsdyna.read.nodout* - Read nodal coord/disp/vel/acc data output 19 | - *lsdyna.read.rbdout* - Read rigid body data output 20 | 21 | 22 | 23 | 24 | 25 | # Example: running simulations 26 | 27 | ## Basic usage (run one simulation): 28 | 29 | ```matlab 30 | S = lsdyna.simulation('C:\FolderToSim\mainFile.k') 31 | S.run 32 | ``` 33 | 34 | ## Multiple simulations (in series): 35 | ```matlab 36 | baseFolder = 'C:\FolderToSims'; 37 | for i = 1:10 38 | simFolder = fullfile(baseFolder,sprintf('sim%d',i)); 39 | S(i) = lsdyna.simulation(fullfile(simFolder,'mainFile.k')); 40 | end 41 | S.run % Each simulation will be run, one after the other 42 | ``` 43 | 44 | ## Multiple simulations (in parallel): 45 | ```matlab 46 | baseFolder = 'C:\FolderToSims'; 47 | for i = 1:10 48 | simFolder = fullfile(baseFolder,sprintf('sim%d',i)); 49 | S(i) = lsdyna.simulation(fullfile(simFolder,'mainFile.k')); 50 | S(i).cmdBlocking = false; 51 | end 52 | % Run simulations in parallel using 4 threads. The first 4 53 | % simulations will start in a new command window, and when each is 54 | % complete, it will fire the next simulation to run in the available 55 | % thread. 56 | S.run('threads',4) 57 | ``` 58 | 59 | # Example: reading ASCII database files 60 | ```matlab 61 | 62 | out = lsdyna.read.asciiFiles(folder) 63 | 64 | out = 65 | asciiFiles with properties: 66 | 67 | folder: 'C:\Folder\Holding\Simulation' 68 | rbdout: [1x1 lsdyna.read.rbdout] 69 | nodfor: [1x1 lsdyna.read.nodfor] 70 | bndout: [1x1 lsdyna.read.bndout] 71 | nodout: [1x1 lsdyna.read.nodout] 72 | elout: [1x1 lsdyna.read.elout] 73 | ``` 74 | 75 | ---------------- 76 | UNDER DEVELOPMENT 77 | ---------------- 78 | Some basic (underlying) utilities for extracting parts, nodes, and elements from kFiles has been created. However, for better extensibility these should be wrapped by a clean object-oriented interface. 79 | 80 | # Example: reading LS-DYNA k-file 81 | ```matlab 82 | kFileStr = 'GHBMC_M50-O_v4-5_20160901.k'; 83 | [PART, NODE, ELEMENT_SHELL, ELEMENT_SOLID] = lsdyna.read.kfile(kFileStr); 84 | figure, plot3(NODE.x,NODE.y,NODE.z,'.'), axis image, view(3) 85 | ``` 86 | 87 | -------------------------------------------------------------------------------- /+lsdyna/+read/asciiFiles.m: -------------------------------------------------------------------------------- 1 | classdef asciiFiles < dynamicprops 2 | %ASCIIFILES Read all available LS-DYNA output databases 3 | % 4 | % out = lsdyna.read.asciiFiles(folder) 5 | % 6 | % out = 7 | % asciiFiles with properties: 8 | % 9 | % folder: 'C:\Folder\Holding\Simulation' 10 | % rbdout: [1x1 lsdyna.read.rbdout] 11 | % nodfor: [1x1 lsdyna.read.nodfor] 12 | % bndout: [1x1 lsdyna.read.bndout] 13 | % nodout: [1x1 lsdyna.read.nodout] 14 | % elout: [1x1 lsdyna.read.elout] 15 | 16 | properties (Transient) 17 | folder 18 | end 19 | 20 | methods 21 | function this = asciiFiles(inputFolder, varargin) 22 | 23 | IP = inputParser; 24 | IP.addParameter('replace', false) 25 | IP.parse(varargin{:}) 26 | opts = IP.Results; 27 | 28 | % Accept a directory (or file) to look for 29 | if exist(inputFolder,'dir') 30 | this.folder = inputFolder; 31 | else 32 | error('Folder %s does not exist',inputFolder) 33 | end 34 | this.refresh 35 | end 36 | function refresh(this,fileName) 37 | 38 | pkgStr = 'lsdyna.read.'; 39 | if nargin<2 40 | % No fileName specified -> refresh all 41 | allMcls = this.getAsciiDbMetaclasses; 42 | for a = 1:numel(allMcls) 43 | this.refresh(allMcls(a)) 44 | end 45 | return; 46 | end 47 | % We need the fileName (elout, bndout, etc) and the metaclass 48 | % object (lsdyna.read.elout, lsdyna.read.bndout, etc) 49 | if ischar(fileName) 50 | mcls = meta.class.fromName([pkgStr fileName]); 51 | elseif isa(fileName,'meta.class') 52 | mcls = fileName; 53 | fileName = mcls.Name(length(pkgStr)+1:end); 54 | else 55 | error('Input must be file name or metaclass') 56 | end 57 | 58 | % Actually try to read this database file 59 | fcn = str2func(mcls.Name); 60 | asciiObj = fcn(this.folder); 61 | 62 | % Add appropriately named properties 63 | % TODO: add method to test if asciiObj actually existed 64 | if isempty(asciiObj) 65 | % TODO: remove a property if it exists 66 | else 67 | if ~isprop(this, fileName) 68 | addprop(this,fileName); 69 | end 70 | this.(fileName) = asciiObj; 71 | end 72 | 73 | end 74 | end 75 | 76 | methods (Static) 77 | function allMcls = getAsciiDbMetaclasses 78 | pkgStr = 'lsdyna.read.'; 79 | % No fileName specified -> refresh all 80 | allFiles = struct2table(dir(fullfile(fileparts(mfilename('fullpath')),'*.m'))); 81 | allFiles.mcls(:,1) = {[]}; 82 | for a = 1:size(allFiles,1) 83 | mcls = meta.class.fromName([pkgStr allFiles.name{a}(1:end-2)]); 84 | if any(strcmp([pkgStr 'DATABASE_FILE'], {mcls.SuperclassList.Name})) 85 | allFiles.mcls{a} = mcls; 86 | end 87 | end 88 | allMcls = cat(1,allFiles.mcls{:}); 89 | end 90 | end 91 | 92 | end -------------------------------------------------------------------------------- /+lsdyna/+read/nodout.m: -------------------------------------------------------------------------------- 1 | classdef nodout < lsdyna.read.DATABASE_FILE 2 | %NODOUT Read a rigid body output (nodout) LS-DYNA ascii file 3 | % nodout = lsdyna.read.nodout(folder) 4 | 5 | properties 6 | file = 'nodout' 7 | NODE_INFO 8 | NODE_DATA 9 | end 10 | methods 11 | function this = nodout(varargin) 12 | this = this@lsdyna.read.DATABASE_FILE(varargin{:}); 13 | end 14 | end 15 | methods (Hidden) 16 | function addDerivedDataChannels(~) 17 | end 18 | function parseFileContents(this, inStr) 19 | 20 | %% 21 | % Find the timestep anchors throughout the file 22 | sciFloatPattern = this.sciNumRegexpPattern; 23 | tStepPattern = ['^ n o d a l p r i n t o u t f o r t i m e s t e p\s*(\d)+\s+\( at time (' sciFloatPattern ') \)']; 24 | [timestepInds,timestepEnds,~,~,te] = regexp(inStr,tStepPattern,'lineanchors'); 25 | te = cat(1,te{:}); 26 | timestepArr = str2num(char(te(1:2:end,2))); %#ok 27 | nTimesteps = length(timestepArr); 28 | 29 | %% 30 | % First check the string chunk between first two timesteps to get a 31 | % template for which channels/elements to expect in rest of file 32 | if isempty(timestepArr) 33 | return 34 | elseif isscalar(timestepArr) 35 | inStrA = inStr(timestepInds(1):end); 36 | else 37 | inStrA = inStr(timestepInds(1):timestepInds(2)); 38 | end 39 | 40 | %% 41 | nodeNoStrs = regexp(inStrA,'^\s*\d+','lineanchors','match'); 42 | nodeIDs = sscanf(sprintf('%s ',nodeNoStrs{:}),'%d'); 43 | nNodes = length(nodeIDs); 44 | this.NODE_INFO = array2table(nodeIDs,'Var',{'NODE_ID'}); 45 | 46 | % Node position data sits in 12 channels: x-disp y-disp z-disp 47 | % x-vel y-vel z-vel x-accl y-accl z-accl x-coor y-coor z-coor 48 | fmtStr = [' %*d' repmat(' %f',1,12)]; 49 | nChPerLine = 2 + 154; 50 | nodDat = sscanf(inStr(bsxfun(@plus, ... 51 | reshape(timestepEnds(1:2:end),1,[])+157,... 52 | (0:nNodes*nChPerLine)')),fmtStr); 53 | % Get data into time-by-channel-by-nodeNum array 54 | nodDat = permute(reshape(nodDat, 12, nNodes, nTimesteps), [3 1 2]); 55 | 56 | % Assign channels 57 | this.NODE_DATA = array2table(timestepArr ,'Var',{'timestep'}); 58 | this.NODE_DATA.disp_xyz = nodDat(:,1:3,:); 59 | this.NODE_DATA.vel_xyz = nodDat(:,4:6,:); 60 | this.NODE_DATA.acc_xyz = nodDat(:,7:9,:); 61 | this.NODE_DATA.coord_xyz = nodDat(:,10:12,:); 62 | 63 | % Node rotational data sits in 12 channels 64 | nChPerLine = 2 + 118; 65 | fmtStr = [' %*d' repmat(' %f',1,9)]; 66 | nodDat = sscanf(inStr(bsxfun(@plus, ... 67 | reshape(timestepEnds(2:2:end),1,[])+124,... 68 | (1:nNodes*nChPerLine)')),fmtStr); 69 | % Get data into time-by-channel-by-nodeNum array 70 | nodDat = permute(reshape(nodDat, 9, nNodes, nTimesteps), [3 1 2]); 71 | 72 | % Assign rotational channels 73 | this.NODE_DATA.disp_rot_xyz = nodDat(:,1:3,:); 74 | this.NODE_DATA.vel_rot_xyz = nodDat(:,4:6,:); 75 | this.NODE_DATA.acc_rot_xyz = nodDat(:,7:9,:); 76 | end 77 | end 78 | end -------------------------------------------------------------------------------- /+lsdyna/+keyword/card_base.m: -------------------------------------------------------------------------------- 1 | classdef card_base < handle & matlab.mixin.Heterogeneous 2 | % lsdyna.keyword.card_base is the parent class for all keyword cards. 3 | % It is a heterogeneous class allowing different card classes to be 4 | % arrayed together. It defines all methods that can be invoked on an 5 | % ARRAY of unlike cards. Typically, those methods do one of two things: 6 | % 1. Iterate through each individual card and invoke a scalar method 7 | % for that card. For example, a C.printData() method that acts on an 8 | % array of different cards C will invoke the C(i).sca_printData() on 9 | % each ith scalar element of C. The sca_printData() method must 10 | % therefore be defined in all subclasses. 11 | % 2. Iterate through each class type and invoke the array method for 12 | % all elements sharing a common class. 13 | 14 | properties 15 | Keyword(1,1) string = "" 16 | String(:,1) string = "" 17 | File(1,1) lsdyna.keyword.file 18 | LineNumber(1,1) uint32 19 | end 20 | 21 | properties (Dependent = true) 22 | ActiveString 23 | end 24 | methods 25 | function X = get.ActiveString(C) 26 | X = C.String(~startsWith(C.String,"$")); 27 | end 28 | end 29 | 30 | %% CONSTRUCTOR UTILITY methods 31 | 32 | methods 33 | function newCards = makeSpecificCards(oldCards) 34 | % Generate specifically defined keyword cards from generic ones 35 | 36 | % All specific keyword cards inherit from lsdyna.keyword.card 37 | supCardClass = ?lsdyna.keyword.card; 38 | % List the *potential* classes to further specify oldCards as 39 | allPackageClasses = supCardClass.ContainingPackage.ClassList; 40 | specificClasses = allPackageClasses(allPackageClasses < supCardClass); 41 | 42 | % Copy cards, replace any whos keyword matches a sub-card 43 | newCards = oldCards; 44 | oldKeywords = [oldCards.Keyword]; 45 | for sc = 1:length(specificClasses) 46 | % Make a temporary empty object of the target class 47 | specClass = specificClasses(sc); 48 | specClassFcn = str2func(specClass.Name); 49 | specClassObj = specClassFcn(); 50 | % Use the KeywordMatch property to find matching cards 51 | matchInds = find(startsWith(oldKeywords,specClassObj.KeywordMatch)); 52 | for i = 1:length(matchInds) 53 | tmp = specClassFcn(oldCards(matchInds(i))); 54 | newCards(matchInds(i)) = tmp; 55 | end 56 | end 57 | end 58 | end 59 | 60 | %% PARSER UTILITY methods 61 | 62 | methods (Sealed) 63 | function strsCell = dataToString(C) 64 | % Requires "sca_dataToString()" to be defined in all subclasses 65 | nC = numel(C); 66 | strsCell = cell(nC,1); 67 | for i = 1:nC 68 | strsCell{i} = sca_dataToString(C(i)); 69 | end 70 | end 71 | function C = stringToData(C) 72 | % Requires "arr_stringToData()" to be defined in all subclasses 73 | 74 | % Find all subclasses of lsdyna.keyword.card in C and invoke 75 | % their arr_stringToData method. 76 | supCardClass = ?lsdyna.keyword.card; 77 | [unqClasses,~,classGrps] = unique(arrayfun(@class, C, 'Un', 0)); 78 | for grp = 1:length(unqClasses) 79 | className = unqClasses{grp}; 80 | if isequal(className,supCardClass.Name) 81 | % Nothing to parse for basic unspecified cards 82 | continue; 83 | end 84 | mask = classGrps==grp; 85 | fprintf("Parsing %s cards [%d] ... ",className,nnz(mask)) 86 | tic 87 | C(mask) = C(mask).arr_stringToData; 88 | fprintf("done in %0.2fs.\n",toc) 89 | end 90 | end 91 | end 92 | end 93 | 94 | -------------------------------------------------------------------------------- /+lsdyna/+read/rbdout.m: -------------------------------------------------------------------------------- 1 | classdef rbdout < lsdyna.read.DATABASE_FILE 2 | %RBDOUT Read a rigid body output (rbdout) LS-DYNA ascii file 3 | % rbdout = lsdyna.read.rbdout(folder) 4 | 5 | properties 6 | file = 'rbdout' 7 | RBD_INFO 8 | RBD_DATA 9 | end 10 | methods 11 | function this = rbdout(varargin) 12 | this = this@lsdyna.read.DATABASE_FILE(varargin{:}); 13 | end 14 | end 15 | methods (Hidden) 16 | function addDerivedDataChannels(~) 17 | end 18 | function parseFileContents(rbdout, inStr) 19 | %% 20 | % Find the timestep anchors throughout the file 21 | sciFloatPattern = rbdout.sciNumRegexpPattern; 22 | tStepPattern = ['^ r i g i d b o d y m o t i o n a t cycle=\s*(\d)+\s+time=\s+(' sciFloatPattern ')']; 23 | [~,~,~,~,te] = regexp(inStr,tStepPattern,'lineanchors'); 24 | te = cat(1,te{:}); 25 | timestepArr = str2num(char(te(:,2))); %#ok 26 | nTimesteps = length(timestepArr); 27 | 28 | %% 29 | % First check the string chunk between first two timesteps to get a 30 | % template for which channels/elements to expect in rest of file 31 | if isempty(timestepArr) 32 | return 33 | end 34 | %% 35 | % Search for rigid body info 36 | [~,rbdEnds,~,~,rbdTxt] = regexp(inStr,'^ (nodal )?rigid body\s*(\d+)','lineanchors'); 37 | rbdTxtPair = cat(1,rbdTxt{:}); 38 | rbdIds = str2num(char(rbdTxtPair(:,2))); %#ok 39 | [unqRbdIds, firstRibId] = unique(rbdIds,'stable'); 40 | nRgdBds = length(unqRbdIds); 41 | 42 | % Store meta-data about each rigid body found 43 | rbdout.RBD_INFO = array2table(unqRbdIds,'Var',{'RBD_ID'}); 44 | rbdout.RBD_INFO.isNodalRB = strcmp('nodal ', rbdTxtPair(firstRibId,1)); 45 | 46 | % Build the string format for ascii text in rbdout file 47 | fmtCell = { 48 | [' coordinates:' repmat(' %f',1,3)] 49 | [' displacements:' repmat(' %f',1,6)] 50 | [' velocities:' repmat(' %f',1,6)] 51 | [' accelerations:' repmat(' %f',1,6)] 52 | ' principal or user defined local coordinate direction vectors' 53 | ' a b c' 54 | [' row 1' repmat(' %f',1,3)] 55 | [' row 2' repmat(' %f',1,3)] 56 | [' row 3' repmat(' %f',1,3)] 57 | ' output in principal or user defined local coordinate directions' 58 | ' a b c a-rot b-rot c-rot' 59 | [' displacements:' repmat(' %f',1,6)] 60 | [' velocities:' repmat(' %f',1,6)] 61 | [' accelerations:' repmat(' %f',1,6)] 62 | }; 63 | % We can fetch ALL of these at once for all rigid bodies. Each 64 | % rigid body's data consists of 48 separate numbers: 65 | fmtStr = [fmtCell{:}]; 66 | rdbDat = sscanf(inStr(bsxfun(@plus, rbdEnds(:)', (91:1128)')),fmtStr); 67 | 68 | % Break into timestep-by-48channels-by-rgdBds array 69 | rdbDat = permute(reshape(rdbDat, 48,nRgdBds, nTimesteps), [3 1 2]); 70 | 71 | % Extract data 72 | rbdout.RBD_DATA = array2table(timestepArr ,'Var',{'timestep'}); 73 | % Coordinates, displacements, velocities, accelerations in xyz 74 | rbdout.RBD_DATA.coord_xyz = rdbDat(:,1:3,:); 75 | rbdout.RBD_DATA.disp_xyz = rdbDat(:,4:6,:); 76 | rbdout.RBD_DATA.disp_rot_xyz = rdbDat(:,7:9,:); 77 | rbdout.RBD_DATA.vel_xyz = rdbDat(:,10:12,:); 78 | rbdout.RBD_DATA.vel_rot_xyz = rdbDat(:,13:15,:); 79 | rbdout.RBD_DATA.acc_xyz = rdbDat(:,16:18,:); 80 | rbdout.RBD_DATA.acc_rot_xyz = rdbDat(:,19:21,:); 81 | % Direction vectors for local abc rigid body coordinate system 82 | rbdout.RBD_DATA.abc_dir_vecs = cellfun(@(x)reshape(x,3,3)',... 83 | permute(num2cell(rdbDat(:,22:30,:), 2), [1 3 2]), 'Un',0); 84 | % Coordinates, displacements, velocities, accelerations in abc 85 | rbdout.RBD_DATA.disp_abc = rdbDat(:,31:33,:); 86 | rbdout.RBD_DATA.disp_rot_abc = rdbDat(:,34:36,:); 87 | rbdout.RBD_DATA.vel_abc = rdbDat(:,37:39,:); 88 | rbdout.RBD_DATA.vel_rot_abc = rdbDat(:,40:42,:); 89 | rbdout.RBD_DATA.acc_abc = rdbDat(:,43:45,:); 90 | rbdout.RBD_DATA.acc_rot_abc = rdbDat(:,46:48,:); 91 | end 92 | end 93 | end -------------------------------------------------------------------------------- /+lsdyna/+keyword/utils.m: -------------------------------------------------------------------------------- 1 | classdef utils 2 | %UNTITLED Summary of this class goes here 3 | % Detailed explanation goes here 4 | 5 | methods (Static = true) 6 | function defns = cardLineDefinition(cardId) 7 | switch upper(cardId) 8 | case "NODE" 9 | FLDS = cell2table({ 10 | 'nid' 'x' 'y' 'z' 'tc' 'rc' 11 | 8 16 16 16 8 8 12 | "d" "f" "f" "f" "f" "f" 13 | }','Var',{'fld','size','fmt'}); 14 | % Note: this is for the 8-node in 1 card definition. 15 | % There is also a 10-node in 2 cards definition not yet 16 | % implemented. 17 | defns = cell2table({ 18 | "NODE" 1 @(i)true(size(i)) FLDS 19 | }, 'Var', {'keyword','lineNo','lineMatchFcn','FLDS'}); 20 | case "ELEMENT_SHELL" 21 | FLDSshell = cell2table({ 22 | 'eid' 'pid' 'n1' 'n2' 'n3' 'n4' 'n5' 'n6' 'n7' 'n8' 23 | 8 8 8 8 8 8 8 8 8 8 24 | "d" "d" "d" "d" "d" "d" "d" "d" "d" "d" 25 | }','Var',{'fld','size','fmt'}); 26 | FLDSshellThick = cell2table({ 27 | 't1' 't2' 't3' 't4' 'beta' 28 | 16 16 16 16 16 29 | "f" "f" "f" "f" "f" 30 | }','Var',{'fld','size','fmt'}); 31 | defns = cell2table({ 32 | "ELEMENT_SHELL" 1 @(i)true(size(i)) FLDSshell 33 | "ELEMENT_SHELL_THICKNESS" 1 @(i)mod(i,2)==1 FLDSshell 34 | "ELEMENT_SHELL_THICKNESS" 2 @(i)mod(i,2)==0 FLDSshellThick 35 | }, 'Var', {'keyword','lineNo','lineMatchFcn','FLDS'}); 36 | case "ELEMENT_SOLID" 37 | % Note: there are old and new formats for the 38 | % ELEMENT_SOLID layout. One has the 8-node in 1 line 39 | % card definition but there is also a 10-node in 2 40 | % lines cards definition. It will be the responsibility 41 | % of the ELEMENT_SOLID.arr_stringToData() function to 42 | % determine which is appropriate. 43 | FLDSsolid = cell2table({ 44 | 'eid' 'pid' 'n1' 'n2' 'n3' 'n4' 'n5' 'n6' 'n7' 'n8' 45 | 8 8 8 8 8 8 8 8 8 8 46 | "d" "d" "d" "d" "d" "d" "d" "d" "d" "d" 47 | }','Var',{'fld','size','fmt'}); 48 | FLDSsolid10_line1 = cell2table({ 49 | 'eid' 'pid' 50 | 8 8 51 | "d" "d" 52 | }','Var',{'fld','size','fmt'}); 53 | FLDSsolid10_line2 = cell2table({ 54 | 'n1' 'n2' 'n3' 'n4' 'n5' 'n6' 'n7' 'n8' 'n9' 'n10' 55 | 8 8 8 8 8 8 8 8 8 8 56 | "d" "d" "d" "d" "d" "d" "d" "d" "d" "d" 57 | }','Var',{'fld','size','fmt'}); 58 | defns = cell2table({ 59 | "ELEMENT_SOLID" 1 @(i)true(size(i)) FLDSsolid 60 | "ELEMENT_SOLID (ten nodes format)" 1 @(i)mod(i,2)==1 FLDSsolid10_line1 61 | "ELEMENT_SOLID (ten nodes format)" 2 @(i)mod(i,2)==0 FLDSsolid10_line2 62 | }, 'Var', {'keyword','lineNo','lineMatchFcn','FLDS'}); 63 | case "ELEMENT_DISCRETE" 64 | FLDS = cell2table({ 65 | 'eid' 'pid' 'n1' 'n2' 'vid' 's' 'pf' 'offset' 66 | 8 8 8 8 8 16 8 16 67 | "d" "d" "d" "d" "d" "f" "d" "f" 68 | }','Var',{'fld','size','fmt'}); 69 | defns = cell2table({ 70 | "ELEMENT_DISCRETE" 1 @(i)true(size(i)) FLDS 71 | }, 'Var', {'keyword','lineNo','lineMatchFcn','FLDS'}); 72 | case "ELEMENT_BEAM" 73 | FLDS = cell2table({ 74 | 'eid' 'pid' 'n1' 'n2' 'n3' 'rt1' 'rr1' 'rt2' 'rr2' 'local' 75 | 8 8 8 8 8 8 8 8 8 8 76 | "d" "d" "d" "d" "d" "d" "d" "d" "d" "d" 77 | }','Var',{'fld','size','fmt'}); 78 | defns = cell2table({ 79 | "ELEMENT_BEAM" 1 @(i)true(size(i)) FLDS 80 | }, 'Var', {'keyword','lineNo','lineMatchFcn','FLDS'}); 81 | end 82 | 83 | % Push some helpful variables into the FLDS table 84 | for mNo = 1:size(defns,1) 85 | FLDS = defns.FLDS{mNo}; 86 | FLDS.startChar = 1+[0;cumsum(FLDS.size(1:end-1))]; 87 | FLDS.endChar = FLDS.startChar + FLDS.size - 1; 88 | FLDS.charInds = arrayfun(@(from,to)... 89 | from:to,FLDS.startChar,FLDS.endChar,'Un',0); 90 | defns.FLDS{mNo} = FLDS; 91 | end 92 | end 93 | end 94 | end 95 | 96 | -------------------------------------------------------------------------------- /+lsdyna/+keyword/ELEMENT_BEAM.m: -------------------------------------------------------------------------------- 1 | classdef ELEMENT_BEAM < lsdyna.keyword.card 2 | %lsdyna.keyword.ELEMENT_BEAM 3 | 4 | properties (Constant) 5 | KeywordMatch = "ELEMENT_BEAM"; 6 | LineDefinitions = ... 7 | lsdyna.keyword.utils.cardLineDefinition("ELEMENT_BEAM"); 8 | end 9 | properties (Constant, Hidden) 10 | DependentCards = "NODE"; 11 | end 12 | 13 | properties 14 | ElemData = table; 15 | end 16 | 17 | %% CONSTRUCTOR 18 | methods 19 | function newCard = ELEMENT_BEAM(basicCard) 20 | % Allow empty constructor 21 | if ~nargin 22 | return; 23 | end 24 | % Elseif isa basicCard then convert 25 | newCard = basicCard.assignPropsToSubclass(newCard); 26 | % Else call superclass constructor on varargin 27 | end 28 | 29 | function C = arr_stringToData(C) 30 | % Parse the string data and populate this card's numeric data 31 | 32 | % Supply definitions for each line of any cards represented by 33 | % this class 34 | lineDefns = C(1).LineDefinitions; 35 | 36 | %% Populate the line definitions with strings from each card 37 | % We will group into individual cards first, then separate into 38 | % the separate lines within each card 39 | [unqKeywords,~,keyGrp] = unique(lineDefns.keyword); 40 | unqKeyT = table(unqKeywords,'Var',{'keyword'}); 41 | unqKeyT.strs(:,1) = {strings(0,1)}; 42 | for grpNo = 1:length(unqKeywords) 43 | m = [C.Keyword]==unqKeywords(grpNo); 44 | unqKeyT.cardMask(grpNo,:) = m(:)'; 45 | if any(m) 46 | unqKeyT.strs(grpNo) = {cat(1,C(m).ActiveString)}; 47 | unqKeyT.lineCounts(grpNo,1) = {cellfun(@numel,{C(m).ActiveString})}; 48 | end 49 | end 50 | for mNo = 1:size(lineDefns,1) 51 | grpNo = keyGrp(mNo); 52 | fullStrs = unqKeyT.strs{grpNo}; 53 | lineMask = lineDefns.lineMatchFcn{mNo}(1:length(fullStrs)); 54 | FLDS = lineDefns.FLDS{mNo}; 55 | lineDefns.strs(mNo,1) = { % Convert comma-separated to spaces 56 | C.convertCommaSepStrsToSpacedStrs(fullStrs(lineMask),FLDS.size)}; 57 | end 58 | 59 | %% Convert each line to data 60 | for mNo = 1:size(lineDefns,1) 61 | strs = lineDefns.strs{mNo}; 62 | FLDS = lineDefns.FLDS{mNo}; 63 | 64 | % Turn comma-sep lines into spaced lines and read the data 65 | strs = C.convertCommaSepStrsToSpacedStrs(strs,FLDS.size); 66 | SHELLDATA = C.convertSpacedStrsToMatrix(strs,FLDS); 67 | SHELL_TABLE = array2table(SHELLDATA,'Var',FLDS.fld); 68 | % Change digit-specified fields to ints (CAREFUL! we don't 69 | % want to change deliberately negative integers so this bit 70 | % should coincide with the keyword card definitions. For 71 | % shells there are no negative element/node ids) 72 | for fldNo = find(strcmp(FLDS.fmt,'d'))' 73 | fld = FLDS.fld{fldNo}; 74 | SHELL_TABLE.(fld) = uint32(SHELL_TABLE.(fld)); 75 | end 76 | lineDefns.DATA_TABLE{mNo,1} = SHELL_TABLE; 77 | end 78 | 79 | %% Combine lines into cards 80 | % Here we have an interesting problem. The ELEMENT_SHELL card 81 | % can be concatenated vertically with line 1 from the 82 | % ELEMENT_SHELL_THICKNESS card, or the two lines from 83 | % ELEMENT_SHELL_THICKNESS can be concatenated horizontally. OR, 84 | % we could add dummy thickness variables to the ELEMENT_SHELL 85 | % cards so that they can all be concatenated. I'm not sure what 86 | % is best. Let's stick with cards being unique. 87 | for grpNo = find(~cellfun(@isempty,unqKeyT.strs)') 88 | % Concatenate lines into one wide data table 89 | DT = [lineDefns.DATA_TABLE{keyGrp==grpNo}]; 90 | % Merge nodeId vars into one var and drop unused nodes 91 | nidFlds = ~cellfun(@isempty,... 92 | regexp(DT.Properties.VariableNames,'^n\d+$')); 93 | DT = mergevars(DT,nidFlds,'NewVariableName','nids'); 94 | DT.nids(:,all(DT.nids==0,1)) = []; 95 | 96 | % For cards with multiple lines that are hori-concatenated 97 | % the row numbers into the data table must be the total 98 | % line numbers divided by the number of lines per card 99 | nLines = nnz(keyGrp==grpNo); 100 | rowsPerCard = unqKeyT.lineCounts{grpNo}/nLines; 101 | cardsEndAt = cumsum(rowsPerCard); 102 | cardsStartAt = [1 cardsEndAt(1:end-1)+1]; 103 | % Push individual separate data tables into each card 104 | CARDset = arrayfun(@(from,to)... 105 | DT(from:to,:),cardsStartAt,cardsEndAt,'Un',0); 106 | [C(unqKeyT.cardMask(grpNo,:)).ElemData] = CARDset{:}; 107 | end 108 | end 109 | end 110 | end -------------------------------------------------------------------------------- /+lsdyna/+keyword/ELEMENT_DISCRETE.m: -------------------------------------------------------------------------------- 1 | classdef ELEMENT_DISCRETE < lsdyna.keyword.card 2 | %lsdyna.keyword.ELEMENT_DISCRETE 3 | 4 | properties (Constant) 5 | KeywordMatch = "ELEMENT_DISCRETE"; 6 | LineDefinitions = ... 7 | lsdyna.keyword.utils.cardLineDefinition("ELEMENT_DISCRETE"); 8 | end 9 | properties (Constant, Hidden) 10 | DependentCards = "NODE"; 11 | end 12 | 13 | properties 14 | ElemData = table; 15 | end 16 | 17 | %% CONSTRUCTOR 18 | methods 19 | function newCard = ELEMENT_DISCRETE(basicCard) 20 | % Allow empty constructor 21 | if ~nargin 22 | return; 23 | end 24 | % Elseif isa basicCard then convert 25 | newCard = basicCard.assignPropsToSubclass(newCard); 26 | % Else call superclass constructor on varargin 27 | end 28 | 29 | function C = arr_stringToData(C) 30 | % Parse the string data and populate this card's numeric data 31 | 32 | % Supply definitions for each line of any cards represented by 33 | % this class 34 | lineDefns = C(1).LineDefinitions; 35 | 36 | %% Populate the line definitions with strings from each card 37 | % We will group into individual cards first, then separate into 38 | % the separate lines within each card 39 | [unqKeywords,~,keyGrp] = unique(lineDefns.keyword); 40 | unqKeyT = table(unqKeywords,'Var',{'keyword'}); 41 | unqKeyT.strs(:,1) = {strings(0,1)}; 42 | for grpNo = 1:length(unqKeywords) 43 | m = [C.Keyword]==unqKeywords(grpNo); 44 | unqKeyT.cardMask(grpNo,:) = m(:)'; 45 | if any(m) 46 | unqKeyT.strs(grpNo) = {cat(1,C(m).ActiveString)}; 47 | unqKeyT.lineCounts(grpNo,1) = {cellfun(@numel,{C(m).ActiveString})}; 48 | end 49 | end 50 | for mNo = 1:size(lineDefns,1) 51 | grpNo = keyGrp(mNo); 52 | fullStrs = unqKeyT.strs{grpNo}; 53 | lineMask = lineDefns.lineMatchFcn{mNo}(1:length(fullStrs)); 54 | FLDS = lineDefns.FLDS{mNo}; 55 | lineDefns.strs(mNo,1) = { % Convert comma-separated to spaces 56 | C.convertCommaSepStrsToSpacedStrs(fullStrs(lineMask),FLDS.size)}; 57 | end 58 | 59 | %% Convert each line to data 60 | for mNo = 1:size(lineDefns,1) 61 | strs = lineDefns.strs{mNo}; 62 | FLDS = lineDefns.FLDS{mNo}; 63 | 64 | % Turn comma-sep lines into spaced lines and read the data 65 | strs = C.convertCommaSepStrsToSpacedStrs(strs,FLDS.size); 66 | SHELLDATA = C.convertSpacedStrsToMatrix(strs,FLDS); 67 | SHELL_TABLE = array2table(SHELLDATA,'Var',FLDS.fld); 68 | % Change digit-specified fields to ints (CAREFUL! we don't 69 | % want to change deliberately negative integers so this bit 70 | % should coincide with the keyword card definitions. For 71 | % shells there are no negative element/node ids) 72 | for fldNo = find(strcmp(FLDS.fmt,'d'))' 73 | fld = FLDS.fld{fldNo}; 74 | SHELL_TABLE.(fld) = uint32(SHELL_TABLE.(fld)); 75 | end 76 | lineDefns.DATA_TABLE{mNo,1} = SHELL_TABLE; 77 | end 78 | 79 | %% Combine lines into cards 80 | % Here we have an interesting problem. The ELEMENT_SHELL card 81 | % can be concatenated vertically with line 1 from the 82 | % ELEMENT_SHELL_THICKNESS card, or the two lines from 83 | % ELEMENT_SHELL_THICKNESS can be concatenated horizontally. OR, 84 | % we could add dummy thickness variables to the ELEMENT_SHELL 85 | % cards so that they can all be concatenated. I'm not sure what 86 | % is best. Let's stick with cards being unique. 87 | for grpNo = find(~cellfun(@isempty,unqKeyT.strs)') 88 | % Concatenate lines into one wide data table 89 | DT = [lineDefns.DATA_TABLE{keyGrp==grpNo}]; 90 | % Merge nodeId vars into one var and drop unused nodes 91 | nidFlds = ~cellfun(@isempty,... 92 | regexp(DT.Properties.VariableNames,'^n\d+$')); 93 | DT = mergevars(DT,nidFlds,'NewVariableName','nids'); 94 | DT.nids(:,all(DT.nids==0,1)) = []; 95 | 96 | % For cards with multiple lines that are hori-concatenated 97 | % the row numbers into the data table must be the total 98 | % line numbers divided by the number of lines per card 99 | nLines = nnz(keyGrp==grpNo); 100 | rowsPerCard = unqKeyT.lineCounts{grpNo}/nLines; 101 | cardsEndAt = cumsum(rowsPerCard); 102 | cardsStartAt = [1 cardsEndAt(1:end-1)+1]; 103 | % Push individual separate data tables into each card 104 | CARDset = arrayfun(@(from,to)... 105 | DT(from:to,:),cardsStartAt,cardsEndAt,'Un',0); 106 | [C(unqKeyT.cardMask(grpNo,:)).ElemData] = CARDset{:}; 107 | end 108 | end 109 | end 110 | end -------------------------------------------------------------------------------- /+lsdyna/+keyword/ELEMENT_SHELL.m: -------------------------------------------------------------------------------- 1 | classdef ELEMENT_SHELL < lsdyna.keyword.card 2 | %lsdyna.keyword.NODE 3 | 4 | properties (Constant) 5 | KeywordMatch = "ELEMENT_SHELL"; 6 | LineDefinitions = ... 7 | lsdyna.keyword.utils.cardLineDefinition("ELEMENT_SHELL"); 8 | end 9 | properties (Constant, Hidden) 10 | DependentCards = "NODE"; 11 | end 12 | 13 | properties 14 | ElemData = table; 15 | end 16 | 17 | %% CONSTRUCTOR 18 | methods 19 | function newCard = ELEMENT_SHELL(basicCard) 20 | % Allow empty constructor 21 | if ~nargin 22 | return; 23 | end 24 | % Elseif isa basicCard then convert 25 | newCard = basicCard.assignPropsToSubclass(newCard); 26 | % Else call superclass constructor on varargin 27 | end 28 | 29 | function C = arr_stringToData(C) 30 | % Parse the string data and populate this card's numeric data 31 | 32 | % Supply definitions for each line of any cards represented by 33 | % this class 34 | lineDefns = C(1).LineDefinitions; 35 | 36 | %% Populate the line definitions with strings from each card 37 | % We will group into individual cards first, then separate into 38 | % the separate lines within each card 39 | [unqKeywords,~,keyGrp] = unique(lineDefns.keyword); 40 | unqKeyT = table(unqKeywords,'Var',{'keyword'}); 41 | unqKeyT.strs(:,1) = {strings(0,1)}; 42 | for grpNo = 1:length(unqKeywords) 43 | m = [C.Keyword]==unqKeywords(grpNo); 44 | unqKeyT.cardMask(grpNo,:) = m(:)'; 45 | if any(m) 46 | unqKeyT.strs(grpNo) = {cat(1,C(m).ActiveString)}; 47 | unqKeyT.lineCounts(grpNo,1) = {cellfun(@numel,{C(m).ActiveString})}; 48 | end 49 | end 50 | for mNo = 1:size(lineDefns,1) 51 | grpNo = keyGrp(mNo); 52 | fullStrs = unqKeyT.strs{grpNo}; 53 | lineMask = lineDefns.lineMatchFcn{mNo}(1:length(fullStrs)); 54 | FLDS = lineDefns.FLDS{mNo}; 55 | lineDefns.strs(mNo,1) = { % Convert comma-separated to spaces 56 | C.convertCommaSepStrsToSpacedStrs(fullStrs(lineMask),FLDS.size)}; 57 | end 58 | 59 | %% Convert each line to data 60 | for mNo = 1:size(lineDefns,1) 61 | strs = lineDefns.strs{mNo}; 62 | FLDS = lineDefns.FLDS{mNo}; 63 | 64 | % Turn comma-sep lines into spaced lines and read the data 65 | strs = C.convertCommaSepStrsToSpacedStrs(strs,FLDS.size); 66 | SHELLDATA = C.convertSpacedStrsToMatrix(strs,FLDS); 67 | SHELL_TABLE = array2table(SHELLDATA,'Var',FLDS.fld); 68 | % Change digit-specified fields to ints (CAREFUL! we don't 69 | % want to change deliberately negative integers so this bit 70 | % should coincide with the keyword card definitions. For 71 | % shells there are no negative element/node ids) 72 | for fldNo = find(strcmp(FLDS.fmt,'d'))' 73 | fld = FLDS.fld{fldNo}; 74 | SHELL_TABLE.(fld) = uint32(SHELL_TABLE.(fld)); 75 | end 76 | lineDefns.DATA_TABLE{mNo,1} = SHELL_TABLE; 77 | end 78 | 79 | %% Combine lines into cards 80 | % Here we have an interesting problem. The ELEMENT_SHELL card 81 | % can be concatenated vertically with line 1 from the 82 | % ELEMENT_SHELL_THICKNESS card, or the two lines from 83 | % ELEMENT_SHELL_THICKNESS can be concatenated horizontally. OR, 84 | % we could add dummy thickness variables to the ELEMENT_SHELL 85 | % cards so that they can all be concatenated. I'm not sure what 86 | % is best. Let's stick with cards being unique. 87 | for grpNo = find(~cellfun(@isempty,unqKeyT.strs)') 88 | % Concatenate lines into one wide data table 89 | DT = [lineDefns.DATA_TABLE{keyGrp==grpNo}]; 90 | % Merge nodeId vars into one var and drop unused nodes 91 | nidFlds = ~cellfun(@isempty,... 92 | regexp(DT.Properties.VariableNames,'^n\d+$')); 93 | DT = mergevars(DT,nidFlds,'NewVariableName','nids'); 94 | DT.nids(:,all(DT.nids==0,1)) = []; 95 | % Merge also for thickness (t1,t2,etc.) vars 96 | thicFlds = ~cellfun(@isempty,... 97 | regexp(DT.Properties.VariableNames,'^t\d+$')); 98 | if any(thicFlds) 99 | DT = mergevars(DT,thicFlds,'NewVariableName','thic'); 100 | DT.thic(:,all(DT.thic==0,1)) = []; 101 | end 102 | 103 | % For cards with multiple lines that are hori-concatenated 104 | % the row numbers into the data table must be the total 105 | % line numbers divided by the number of lines per card 106 | nLines = nnz(keyGrp==grpNo); 107 | rowsPerCard = unqKeyT.lineCounts{grpNo}/nLines; 108 | cardsEndAt = cumsum(rowsPerCard); 109 | cardsStartAt = [1 cardsEndAt(1:end-1)+1]; 110 | % Push individual separate data tables into each card 111 | CARDset = arrayfun(@(from,to)... 112 | DT(from:to,:),cardsStartAt,cardsEndAt,'Un',0); 113 | [C(unqKeyT.cardMask(grpNo,:)).ElemData] = CARDset{:}; 114 | end 115 | end 116 | end 117 | end -------------------------------------------------------------------------------- /+lsdyna/+read/DATABASE_FILE.m: -------------------------------------------------------------------------------- 1 | classdef DATABASE_FILE < handle 2 | %DATABASE_FILE Superclass for all LS-DYNA ascii database file readers 3 | 4 | properties (Transient) 5 | folder 6 | source 7 | end 8 | properties (Abstract) 9 | file 10 | end 11 | properties (Constant, Hidden) 12 | storageMatFile = 'lsdyna.database.mat' 13 | sciNumRegexpPattern = '[-+]?[0-9]*\.?[0-9]+([eE][-+]?[0-9]+)?'; 14 | end 15 | properties (Hidden) 16 | storageMatFileLastRead 17 | end 18 | properties (Dependent, Hidden) 19 | asciiFullfile 20 | storageMatFullfile 21 | end 22 | methods %% Utility methods 23 | function x = get.asciiFullfile(this) 24 | x = fullfile(this.folder, this.file); 25 | end 26 | function x = get.storageMatFullfile(this) 27 | x = fullfile(this.folder, this.storageMatFile); 28 | end 29 | end 30 | methods (Abstract, Hidden) %% Required for any child classes to implement 31 | parseFileContents(this, inStr) 32 | addDerivedDataChannels(this) 33 | end 34 | 35 | methods 36 | % Constructor, called by all children: 37 | % this = this@lsdyna.read.DATABASE_FILE(varargin{:}); 38 | function this = DATABASE_FILE(inputFolder,varargin) 39 | 40 | % Accept a directory (or file) to look for 41 | if exist(inputFolder,'dir') 42 | this.folder = inputFolder; 43 | elseif exist(inputFolder,'file') 44 | [this.folder,b,c] = fileparts(inputFolder); 45 | this.file = [b c]; 46 | else 47 | warning('File %s does not exist - attempting to load from .mat file',inputFolder) 48 | this.folder = fileparts(inputFolder); 49 | end 50 | 51 | % By default presume we need to load from ascii 52 | loadFromMatFile = false; 53 | % Check for a previously saved .mat file 54 | [savedObj, lastReadDate] = loadMatContents(this); 55 | 56 | % Check if there's an ascii file to load (or compare dates) 57 | if exist(this.asciiFullfile,'file') && ~isempty(savedObj) 58 | asciiMeta = dir(this.asciiFullfile); 59 | if ~isempty(lastReadDate) && lastReadDate>datetime(asciiMeta.date) 60 | loadFromMatFile = true; 61 | end 62 | else % No ascii file - just rely on .mat file 63 | if ~isempty(savedObj) 64 | loadFromMatFile = true; 65 | end 66 | end 67 | 68 | if loadFromMatFile 69 | mc = metaclass(savedObj); 70 | for i = 1:length(mc.PropertyList) 71 | prop = mc.PropertyList(i); 72 | % Ignore Transient properties like the folder 73 | % name (which will be empty when saved) 74 | if prop.Transient || prop.NonCopyable 75 | continue; 76 | end 77 | this.(prop.Name) = savedObj.(prop.Name); 78 | end 79 | this.source = 'mat'; 80 | else % Resort to a load from ascii 81 | if exist(this.asciiFullfile,'file') 82 | this.readAscii; 83 | this.source = 'ascii'; 84 | else 85 | warning('File not found: %s\n',this.asciiFullfile) 86 | return; 87 | end 88 | end 89 | 90 | % Append any derived data fields 91 | this.addDerivedDataChannels(); 92 | end 93 | function save(this) 94 | preSaveFile = this.storageMatFullfile; 95 | fprintf('Saving %s to %s...', this.file, preSaveFile) 96 | if exist(preSaveFile,'file') 97 | saveStruct = load(preSaveFile); 98 | else 99 | saveStruct = struct(); 100 | end 101 | saveStruct.(this.file) = this; %#ok 102 | save(preSaveFile, '-struct', 'saveStruct') 103 | fprintf(' done.\n') 104 | end 105 | function [objFromMat, lastReadDate] = loadMatContents(this) 106 | 107 | lastReadDate = datetime('01-01-1900','InputFormat','dd-MM-yyyy'); 108 | objFromMat = []; 109 | % Check for the presence of a pre-saved .mat file to load from 110 | preSaveFile = this.storageMatFullfile; 111 | if exist(preSaveFile,'file') 112 | % Check if this particular database file is pre-saved 113 | savedObjs = whos('-file',preSaveFile); 114 | if any(strcmp(this.file,{savedObjs.name})) 115 | % Attempt to load from .mat file and not ascii file 116 | fprintf('Loading %s from %s...', this.file, preSaveFile) 117 | tmp = load(preSaveFile, this.file); 118 | fprintf(' done.\n') 119 | % We now have loaded object. Copy its copy-able props 120 | objFromMat = tmp.(this.file); 121 | % Extract the last date this object was saved 122 | try 123 | lastReadDate = objFromMat.storageMatFileLastRead; 124 | catch ME 125 | warning(ME.message) 126 | end 127 | end 128 | end 129 | end 130 | function readAscii(this) 131 | fprintf('Loading %s from %s...', this.file, this.asciiFullfile) 132 | inStr = fileread(this.asciiFullfile); 133 | this.parseFileContents(inStr) 134 | this.storageMatFileLastRead = datetime('now'); 135 | fprintf(' done.\n') 136 | this.source = 'ascii'; 137 | end 138 | function replaceAscii(this) 139 | this.save 140 | this.deleteAscii; 141 | end 142 | function deleteAscii(this) 143 | % Remove the ascii file if requested 144 | if exist(this.asciiFullfile,'file') 145 | delete(this.asciiFullfile) 146 | end 147 | end 148 | end 149 | end -------------------------------------------------------------------------------- /+lsdyna/+keyword/card.m: -------------------------------------------------------------------------------- 1 | classdef card < lsdyna.keyword.card_base 2 | %UNTITLED Summary of this class goes here 3 | % Detailed explanation goes here 4 | 5 | %% CONSTRUCTOR 6 | methods 7 | function this = card(Keyword,String,varargin) 8 | % Allow empty constructor 9 | if ~nargin 10 | return; 11 | end 12 | 13 | % Ensure strings for proper iteration 14 | Keyword = string(Keyword); 15 | 16 | % Allow multiple cards at once 17 | if ~isscalar(Keyword) 18 | nCards = numel(Keyword); 19 | this(nCards) = lsdyna.keyword.card(); 20 | for i = 1:nCards 21 | this(i) = lsdyna.keyword.card(Keyword(i),String{i}); 22 | end 23 | return; 24 | end 25 | 26 | % Instantiate single object 27 | this.Keyword = Keyword; 28 | this.String = String; 29 | %this.String = split(String,newline); 30 | end 31 | end 32 | 33 | methods 34 | function strs = sca_dataToString(C) 35 | % By default return the card's String (no data referenced) 36 | strs = C.String; 37 | end 38 | end 39 | 40 | %% CONSTRUCTOR UTILITY methods 41 | 42 | methods (Hidden) 43 | function subclassObj = assignPropsToSubclass(this, subclassObj) 44 | % A utility method to assign all properties of this parent 45 | % class to a given (newly instantiated, likely empty, subclass) 46 | parentMC = metaclass(this); 47 | parentProps = parentMC.PropertyList; 48 | for i = find(~[parentProps.Dependent]) 49 | propName = parentProps(i).Name; 50 | subclassObj.(propName) = this.(propName); 51 | end 52 | end 53 | end 54 | 55 | %% PARSER UTILITY methods 56 | methods (Static) 57 | function strs = convertCommaSepStrsToSpacedStrs(strs,charSpaces) 58 | % Utility function that takes an array of strings and replaces 59 | % those elements with comma-separated values (one style of 60 | % lsdyna deck input) with the more standard fixed-position 61 | % input with the number of characters per data field provided. 62 | 63 | % First find the strings that actually need adjusting 64 | hasCommaInds = find(contains(strs,","))'; 65 | if isempty(hasCommaInds) 66 | return; 67 | end 68 | charSpaces = charSpaces(:)'; 69 | % Next, make a char array (this will auto-pad on the right) 70 | strsChar = char(strs(hasCommaInds)); 71 | % Particularly for massive chunks of text it is very slow to 72 | % call strsplit on commas for each line. Instead, break the big 73 | % chunk of text into chunks that have the commas in the exact 74 | % same left-t-right index along the char array. 75 | [unqCommasMask,~,grps] = unique(strsChar==',','rows'); 76 | for grpNo = 1:size(unqCommasMask,1) 77 | % All lines now have commas in the same place. Do a manual 78 | % split by picking out the text before/after each comma 79 | commaInds = find(unqCommasMask(grpNo,:)); 80 | fromInds = [1 commaInds+1]; 81 | toInds = [commaInds-1 size(unqCommasMask,2)]; 82 | unqStrChars = strsChar(grps==grpNo,:); 83 | % Pick out the pieces of each delimited set of chars 84 | nPieces = length(fromInds); 85 | pieces = strtrim(arrayfun(@(from,to)... 86 | unqStrChars(:,from:to),fromInds,toInds,'Un',0)); 87 | if any(cellfun(@(x)size(x,2),pieces) > charSpaces(1:nPieces)) 88 | error("lsdyna:badFormText",strjoin([ 89 | "Text delimited by commas had more characters" 90 | "than the defined size of the card."])) 91 | end 92 | for pieceNo = 1:nPieces 93 | pieces{pieceNo}(:,end+1:charSpaces(pieceNo)) = ' '; 94 | end 95 | strs(hasCommaInds(grps==grpNo)) = string([pieces{:}]); 96 | end 97 | end 98 | 99 | function DATAMAT = convertSpacedStrsToMatrix(strs,FLDS) 100 | % Convert an array of strings into a matrix of data 101 | 102 | nFlds = height(FLDS); 103 | % Convert strings to char matrix for use in sscanf which is the 104 | % fastest way to bulk-read fixed width data. 105 | charMat = char(strs); 106 | % And ensure that the charMat is at least as wide as expected 107 | charMat(:,FLDS.endChar(end)+1) = ' '; 108 | % MATLAB, however, is terrible at true fixed-width reading 109 | % because it doesn't acknowledge spaces as taking up width (see 110 | % https://www.mathworks.com/matlabcentral/answers/110381). So 111 | % we need to artificially add a default (0) value for fields 112 | % that are total whitespace: 113 | charMat(charMat==char(0)) = ' '; 114 | for i = 1:nFlds 115 | emptyMask = all(charMat(:,FLDS.charInds{i}) == ' ',2); 116 | charMat(emptyMask,FLDS.endChar(i)) = '0'; 117 | end 118 | % and we need to add an extra separator character between 119 | % fields in case the full field width is taken up by data such 120 | % as '1111 23333' which becomes [1111 2333 3] instead of 121 | % [1111 2 3333] if we don't add a separator. We therefore have 122 | % source colNos (into charMat) and shifted targetColNos. 123 | srcCols = [FLDS.charInds{:}]; 124 | % Columns will shift right with one extra char per field 125 | colPads = num2cell(cumsum(0:nFlds-1)'); 126 | tarColInds = cellfun(@(o,e)o+e,FLDS.charInds,colPads,'Un',0); 127 | tarCols = [tarColInds{:}]; 128 | % Initialize the separated char mat with spaces and then fill 129 | charMatAndSep = repmat(' ',size(charMat,1), tarCols(end)+1); 130 | charMatAndSep(:,tarCols) = charMat(:,srcCols); 131 | % Now we can finally use sscanf to read the data 132 | fmtStr = strjoin("%" + FLDS.size + FLDS.fmt," ") + " "; 133 | DATAMAT = reshape(sscanf(charMatAndSep',fmtStr), nFlds,[])'; 134 | end 135 | end 136 | end 137 | 138 | -------------------------------------------------------------------------------- /+lsdyna/+keyword/ELEMENT_SOLID.m: -------------------------------------------------------------------------------- 1 | classdef ELEMENT_SOLID < lsdyna.keyword.card 2 | %lsdyna.keyword.NODE 3 | 4 | properties (Constant) 5 | KeywordMatch = "ELEMENT_SOLID"; 6 | LineDefinitions = ... 7 | lsdyna.keyword.utils.cardLineDefinition("ELEMENT_SOLID"); 8 | end 9 | properties (Constant, Hidden) 10 | DependentCards = "NODE"; 11 | end 12 | 13 | properties 14 | ElemData = table; 15 | end 16 | 17 | methods 18 | function strs = sca_dataToString(C) 19 | %% 20 | FLDS = C.LineDefinitions.FLDS{1}; 21 | DATA = table2array(C.ElemData); % OK because all fields are ints? 22 | 23 | printSpec = strjoin("%0" + FLDS.size + FLDS.fmt,"") + newline; 24 | strs = sprintf(printSpec, DATA'); 25 | strs = splitlines(strs); 26 | strs = strs(1:end-1); 27 | end 28 | end 29 | 30 | %% CONSTRUCTOR 31 | methods 32 | function newCard = ELEMENT_SOLID(basicCard) 33 | % Allow empty constructor 34 | if ~nargin 35 | return; 36 | end 37 | % Elseif isa basicCard then convert 38 | newCard = basicCard.assignPropsToSubclass(newCard); 39 | % Else call superclass constructor on varargin 40 | end 41 | 42 | function C = arr_stringToData(C) 43 | % Parse the string data and populate this card's numeric data 44 | 45 | % Supply definitions for each line of any cards represented by 46 | % this class. 47 | 48 | % NOTE: This currently assumes that a card with keyword 49 | % "ELEMENT_SOLID" refers ONLY the old style of solid element 50 | % input with 1 card (eid,pid,n1-n8) whereas there is a new 51 | % style with 2-line input where line 1 is (eid,pid) and line 2 52 | % is (n1-n10). The GHBMC model used a longer keyword of 53 | % "ELEMENT_SOLID (ten nodes format)" for these cards so for now 54 | % we will just use this to separate the two formats. It seems 55 | % that both formats are valid in the LS-Dyna manual, so what is 56 | % most likely needed is to inspect the actual contents of any 57 | % ELEMENT_SOLID card to determine if the first line contains 58 | % only two (eid, pid) values. 59 | lineDefns = C(1).LineDefinitions; 60 | %% Populate the line definitions with strings from each card 61 | % We will group into individual cards first, then separate into 62 | % the separate lines within each card 63 | [unqKeywords,~,keyGrp] = unique(lineDefns.keyword); 64 | unqKeyT = table(unqKeywords,'Var',{'keyword'}); 65 | for grpNo = 1:length(unqKeywords) 66 | m = [C.Keyword]==unqKeywords(grpNo); 67 | unqKeyT.cardMask(grpNo,:) = m(:)'; 68 | unqKeyT.strs(grpNo,1) = {cat(1,C(m).ActiveString)}; 69 | unqKeyT.lineCounts(grpNo,1) = {cellfun(@numel,{C(m).ActiveString})}; 70 | end 71 | for mNo = 1:size(lineDefns,1) 72 | grpNo = keyGrp(mNo); 73 | fullStrs = unqKeyT.strs{grpNo}; 74 | if isempty(fullStrs) 75 | lineDefns.strs(mNo,1) = {[]}; 76 | continue; 77 | end 78 | lineMask = lineDefns.lineMatchFcn{mNo}(1:length(fullStrs)); 79 | FLDS = lineDefns.FLDS{mNo}; 80 | lineDefns.strs(mNo,1) = { % Convert comma-separated to spaces 81 | C.convertCommaSepStrsToSpacedStrs(fullStrs(lineMask),FLDS.size)}; 82 | end 83 | 84 | %% Convert each line to data 85 | for mNo = find(~cellfun(@isempty,lineDefns.strs))' 86 | strs = lineDefns.strs{mNo}; 87 | FLDS = lineDefns.FLDS{mNo}; 88 | 89 | % Turn comma-sep lines into spaced lines and read the data 90 | strs = C.convertCommaSepStrsToSpacedStrs(strs,FLDS.size); 91 | RAWDATA = C.convertSpacedStrsToMatrix(strs,FLDS); 92 | TABLE_DATA = array2table(RAWDATA,'Var',FLDS.fld); 93 | % Change digit-specified fields to ints (CAREFUL! we don't 94 | % want to change deliberately negative integers so this bit 95 | % should coincide with the keyword card definitions. For 96 | % element_[X] there are no negative element/node ids) 97 | for fldNo = find(strcmp(FLDS.fmt,'d'))' 98 | fld = FLDS.fld{fldNo}; 99 | TABLE_DATA.(fld) = uint32(TABLE_DATA.(fld)); 100 | end 101 | lineDefns.DATA_TABLE{mNo,1} = TABLE_DATA; 102 | end 103 | 104 | %% Combine lines into cards 105 | for grpNo = 1:length(unqKeywords) 106 | % Concatenate lines into one wide data table 107 | DT = [lineDefns.DATA_TABLE{keyGrp==grpNo}]; 108 | if isempty(DT) 109 | continue; 110 | end 111 | % Merge nodeId vars into one var and drop unused nodes 112 | nidFlds = ~cellfun(@isempty,... 113 | regexp(DT.Properties.VariableNames,'^n\d+$')); 114 | DT = mergevars(DT,nidFlds,'NewVariableName','nids'); 115 | DT.nids(:,all(DT.nids==0,1)) = []; 116 | % Merge also for thickness (t1,t2,etc.) vars 117 | thicFlds = ~cellfun(@isempty,... 118 | regexp(DT.Properties.VariableNames,'^t\d+$')); 119 | if any(thicFlds) 120 | DT = mergevars(DT,thicFlds,'NewVariableName','thic'); 121 | DT.thic(:,all(DT.thic==0,1)) = []; 122 | end 123 | 124 | % For cards with multiple lines that are hori-concatenated 125 | % the row numbers into the data table must be the total 126 | % line numbers divided by the number of lines per card 127 | nLines = nnz(keyGrp==grpNo); 128 | rowsPerCard = unqKeyT.lineCounts{grpNo}/nLines; 129 | cardsEndAt = cumsum(rowsPerCard); 130 | cardsStartAt = [1 cardsEndAt(1:end-1)+1]; 131 | % Push individual separate data tables into each card 132 | CARDset = arrayfun(@(from,to)... 133 | DT(from:to,:),cardsStartAt,cardsEndAt,'Un',0); 134 | [C(unqKeyT.cardMask(grpNo,:)).ElemData] = CARDset{:}; 135 | end 136 | end 137 | end 138 | end -------------------------------------------------------------------------------- /+lsdyna/+keyword/file.m: -------------------------------------------------------------------------------- 1 | classdef file < handle 2 | %An LS-Dyna keyword file class 3 | % Detailed explanation goes here 4 | 5 | properties 6 | Filepath(1,1) string = "" 7 | Filename(1,1) string = "" 8 | Preamble(:,1) string = "" 9 | Cards(:,1) lsdyna.keyword.card_base 10 | end 11 | 12 | %% CONSTRUCTOR 13 | methods 14 | function F = file(filename,varargin) 15 | if ~nargin 16 | % Make an empty file 17 | return; 18 | end 19 | [F.Filepath,fname,fext] = fileparts(char(filename)); 20 | F.Filename = [fname fext]; 21 | % Add potential custom input 22 | IP = inputParser; 23 | IP.addParameter('Cards',[]) 24 | IP.addParameter('Preamble',"") 25 | IP.parse(varargin{:}) 26 | givenFields = setdiff(IP.Parameters,IP.UsingDefaults); 27 | for i = 1:length(givenFields) 28 | fld = givenFields{i}; 29 | F.(fld) = IP.Results.(fld); 30 | end 31 | 32 | end 33 | 34 | function makeSpecificCards(F) 35 | % "Dive down" from the most generic card definition to apply 36 | % specific card classes to all cards where available 37 | F.Cards = makeSpecificCards(F.Cards); 38 | end 39 | 40 | function NodeT = getNodesTable(KF) 41 | % Obtain all node data in a single table 42 | C = KF.Cards(startsWith([KF.Cards.Keyword],"NODE","ignoreCase",true)); 43 | NodeT = cat(1,C.NodeData); 44 | end 45 | function PartT = getPartsTable(KF) 46 | % Obtain all part data in a single table 47 | C = KF.Cards(startsWith([KF.Cards.Keyword],"PART","ignoreCase",true)); 48 | PartT = table(uint32([C.PID]'), [C.Heading]', ... 49 | uint32([C.MID]'), uint32([C.SID]'), C(:), 'Var',{ 50 | 'pid','heading','mid','sid','card'}); 51 | end 52 | function ElemT = getElementsTable(KF) 53 | %% Obtain all (or most) element data in a single table 54 | C = KF.Cards(startsWith([KF.Cards.Keyword],"ELEMENT","ignoreCase",true)); 55 | ElemCardKeys = categorical([C.Keyword]'); 56 | ElemCell = arrayfun(@(C)C.ElemData,C,'Un',0,'Err',@(a,b)[]); 57 | % Note that here we're dropping unknown element types, as well 58 | % as any element properties other than eid, pid, nids 59 | nodesPerCell = cellfun(@(x)size(x.nids,2),ElemCell,'Err',@(a,b)0); 60 | maxNodesCount = max(nodesPerCell); 61 | for i = 1:numel(ElemCell) 62 | if isempty(ElemCell{i}) 63 | continue; 64 | end 65 | ElemCell{i}.nids(:,end+1:maxNodesCount) = 0; 66 | ElemCell{i} = ElemCell{i}(:,["eid" "pid" "nids"]); 67 | ElemCell{i}.keyword(:,1) = ElemCardKeys(i); 68 | end 69 | ElemT = cat(1,ElemCell{:}); 70 | 71 | % It will be useful to obtain the TYPE of element, namely TRIA 72 | % (3 noded 2d triangle), QUAD (4 noded 2d), TETRA (4 noded 3d), 73 | % PYRAMID (5 noded 3d), HEX (8 noded 3d) or OTHER 74 | ElemT.elemType(:,1) = categorical(""); 75 | numUnqNodes = ones(height(ElemT),1); 76 | for nodeNo = 2:maxNodesCount 77 | isNewNode = ElemT.nids(:,nodeNo) ~= 0 & ... 78 | ~any(ElemT.nids(:,nodeNo) == ElemT.nids(:,1:nodeNo-1),2); 79 | numUnqNodes(isNewNode) = numUnqNodes(isNewNode) + 1; 80 | end 81 | ElemT.nodeCount = categorical(numUnqNodes); 82 | [unqCnts,~,unqGrp] = unique(numUnqNodes); 83 | isShell = contains(string(ElemT.keyword),"SHELL",'ignoreCase',true); 84 | isSolid = contains(string(ElemT.keyword),"SOLID",'ignoreCase',true); 85 | for i = 1:length(unqCnts) 86 | m = unqGrp==i; 87 | switch unqCnts(i) 88 | case 1 89 | ElemT.elemType(m) = categorical("1d"); 90 | case 2 91 | ElemT.elemType(m) = categorical("2d"); 92 | case 3 93 | ElemT.elemType(m & isShell) = categorical("tria"); 94 | ElemT.elemType(m & ~isShell) = categorical("2d_oriented"); 95 | case 4 96 | ElemT.elemType(m & isShell) = categorical("quad"); 97 | ElemT.elemType(m & isSolid) = categorical("tetra"); 98 | case 5 99 | ElemT.elemType(m & isSolid) = categorical("pyramid"); 100 | case 6 101 | ElemT.elemType(m & isSolid) = categorical("triprism"); 102 | case 8 103 | ElemT.elemType(m & isSolid) = categorical("hex"); 104 | end 105 | end 106 | end 107 | 108 | function append(KF, varargin) 109 | % Append one or more KFILES via concatenating their Cards. 110 | for i = 1:length(varargin) 111 | KF.Cards = cat(1,KF.Cards,varargin{i}.Cards); 112 | end 113 | end 114 | end 115 | 116 | 117 | 118 | methods (Static) 119 | function F = readKfile(FILE,firstNlines) 120 | % readKfile(FILE) % Read the keyword FILE 121 | % readKfile(STRING) % Read the keywords from the given STRING 122 | % readKfile(...,NLINES) % Read just NLINES lines (for testing) 123 | 124 | % Determine the input syntax and read 125 | if (isStringScalar(FILE) || ischar(FILE)) && strlength(FILE) < 1000 126 | assert(exist(FILE,'file')>0,"File not found: %s\n",FILE) 127 | filename = FILE; 128 | tic 129 | fprintf("Reading %s ... ", FILE) 130 | X = string(fileread(FILE)); 131 | fprintf("read %0.0fK chars in %0.2fs.\n", strlength(X)/1000, toc) 132 | else 133 | filename = 'Untitled.k'; 134 | X = string(FILE); 135 | end 136 | 137 | % Split into individual lines 138 | if isscalar(X) 139 | fprintf("Splitting contents ... ") 140 | tic 141 | X = splitlines(X); 142 | fprintf("found %d lines in %0.2fs.\n", numel(X), toc) 143 | end 144 | 145 | % Determine where keywords begin 146 | fprintf("Reading keywords ... ") 147 | tic 148 | if nargin<2 149 | firstNlines = length(X); 150 | end 151 | tmp = X(1:firstNlines); 152 | keywordLineNos = find(startsWith(tmp,'*')); 153 | fprintf("%d read in %0.2fs\n", numel(keywordLineNos), toc) 154 | 155 | % Collect keyword strings and card contents strings 156 | fprintf("Building keywords list ... ") 157 | tic 158 | preComments = tmp(1:keywordLineNos(1)-1); 159 | keywords = strip(deblank(tmp(keywordLineNos)), 'left',"*"); 160 | cardsStart = keywordLineNos + 1; 161 | cardsEnd = [keywordLineNos(2:end)-1; length(tmp)]; 162 | cardsCell = arrayfun(@(from,to)tmp(from:to),cardsStart,cardsEnd,'Un',0); 163 | fprintf(" done in %0.2fs\n", toc) 164 | 165 | % Build the card objects 166 | fprintf("Building basic cards ... ") 167 | tic 168 | cards = lsdyna.keyword.card(keywords,cardsCell); 169 | fprintf("done in %0.2fs\n", toc) 170 | 171 | % Build the kfile object and populate it 172 | F = lsdyna.keyword.file(filename,'Cards',cards,'Preamble',preComments); 173 | % It's quickest to assign all cards a line number at once 174 | lineNos = num2cell(uint32(cardsStart)); 175 | [F.Cards.LineNumber] = lineNos{:}; 176 | [F.Cards.File] = deal(F); 177 | 178 | fprintf("Building specific cards ... ") 179 | tic 180 | F.makeSpecificCards; 181 | fprintf("done in %0.2fs\n", toc) 182 | 183 | F.Cards.stringToData; 184 | 185 | end 186 | end 187 | end 188 | 189 | -------------------------------------------------------------------------------- /+lsdyna/+read/elout.m: -------------------------------------------------------------------------------- 1 | classdef elout < lsdyna.read.DATABASE_FILE 2 | %ELOUT Read element stress/strain output LS-DYNA ascii file 3 | % elout = lsdyna.read.elout(folder) 4 | 5 | properties 6 | file = 'elout' 7 | SHELL_ELEM_INFO 8 | SHELL_DATA 9 | BEAM_ELEM_INFO 10 | BEAM_DATA 11 | end 12 | 13 | methods 14 | function this = elout(varargin) 15 | this = this@lsdyna.read.DATABASE_FILE(varargin{:}); 16 | end 17 | function addDerivedDataChannels(this) 18 | 19 | if ~isempty(this.SHELL_DATA) && ~any(strcmp('stress_vm',this.SHELL_DATA.Properties.VariableNames)) 20 | xx = this.SHELL_DATA.sig_xx; 21 | yy = this.SHELL_DATA.sig_yy; 22 | zz = this.SHELL_DATA.sig_zz; 23 | xy = this.SHELL_DATA.sig_xy; 24 | yz = this.SHELL_DATA.sig_yz; 25 | zx = this.SHELL_DATA.sig_zx; 26 | this.SHELL_DATA.stress_vm = sqrt(0.5 * ... 27 | ((xx-yy).^2 + (yy-zz).^2 + (zz-xx).^2 + 6*(xy.^2+yz.^2+zx.^2))); 28 | end 29 | end 30 | function parseFileContents(this, inStr) 31 | 32 | %% 33 | % Find the timestep anchors throughout the file 34 | tStepPattern = ['t i m e s t e p\s*(\d)+\s+\( at time (' ... 35 | this.sciNumRegexpPattern ') \)']; 36 | [timestepInds,~,~,~,te] = regexp(inStr,tStepPattern,'lineanchors'); 37 | te = cat(1,te{:}); 38 | timestepArr = str2num(char(te(:,2))); %#ok 39 | nTimesteps = length(timestepArr); 40 | 41 | %% 42 | % First check the string chunk between first two timesteps to get a 43 | % template for which channels/elements to expect in rest of file 44 | if isempty(timestepArr) 45 | return 46 | elseif isscalar(timestepArr) 47 | inStrA = inStr(timestepInds(1):end); 48 | else 49 | inStrA = inStr(timestepInds(1):timestepInds(2)); 50 | end 51 | 52 | % Start a SHELL_DATA table 53 | this.SHELL_DATA = array2table(timestepArr ,'Var',{'timestep'}); 54 | 55 | % Find any header (stress) lines 56 | [strsStarts,strsEnds] = regexp(inStrA,' ipt-shl stress.*?\r\n\r\n'); 57 | if ~isempty(strsStarts) 58 | stressStr = inStrA(strsStarts:strsEnds); 59 | 60 | % Fetch headers 61 | stressHdrs2line = stressStr(bsxfun(@plus,0:83, [20; 125])); 62 | stressHdrsCellStrs = arrayfun(@(i)stressHdrs2line(:,(1:12) + (i-1)*12),1:size(stressHdrs2line,2)/12,'Un',0); 63 | stressHdrs = cellfun(@(tmp)matlab.lang.makeValidName(reshape(tmp',1,[])), stressHdrsCellStrs,'Un',0); 64 | nChannels = length(stressHdrs); 65 | 66 | % Fetch elem and mat Ids 67 | [elFrom,~,~,~,elToks] = regexp(stressStr,'^\s*(\d+)-\s*(\d+)\s$','lineanchors'); 68 | elToks = cat(1,elToks{:}); 69 | % We have an Nelem-by-2 cell of integer strings. Get ints. 70 | shellIdsMatIds = reshape(sscanf(sprintf('%s ',elToks{:}),'%d'),[],2); 71 | this.SHELL_ELEM_INFO = array2table(shellIdsMatIds,... 72 | 'Var',{'ELEM_ID','MAT_NO'}); 73 | nElems = size(shellIdsMatIds,1); 74 | 75 | % Count how many integration points per element (note we 76 | % are presuming all elements have same number, things will 77 | % probably break if this is not true) 78 | if nElems>1 79 | elemStr = stressStr(1:elFrom(2)); 80 | else 81 | elemStr = stressStr; 82 | end 83 | % Number of integration points is the number of lines like 84 | % 1- 2 elastic... 85 | % 2- 2 elastic... 86 | nIP = length(regexp(elemStr,'\d+-\s*\d+\s*[a-zA-Z]')); 87 | 88 | % We now know the number of elements and the number of 89 | % integration points. Build the string format specifier. 90 | nChNL = 2; 91 | nChElemLine = 16 + nChNL; 92 | nChIPLine = 103 + nChNL; 93 | nChElem = nChElemLine + nIP*nChIPLine; 94 | fmtChannels = repmat(' %f',1,nChannels); 95 | fmtIPline = ['%*u- %*u elastic' fmtChannels]; 96 | fmtElem = ['%*u- %*u ' repmat(fmtIPline,1,nIP)]; 97 | 98 | % Gather stress as channels-by-IP-by-elem-by-time array 99 | stressData = zeros(nChannels, nIP, nElems, nTimesteps,'single'); 100 | for i = 1:nTimesteps 101 | tmpStr = inStr(timestepInds(i) + 291 + (1:nChElem*nElems)); 102 | stressData(:,:,:,i) = reshape(sscanf(tmpStr,fmtElem),nChannels,nIP,nElems); 103 | end 104 | 105 | end 106 | 107 | % Unpack to shell data table 108 | for hdrNo = 1:length(stressHdrs) 109 | this.SHELL_DATA.(stressHdrs{hdrNo}) = permute(stressData(hdrNo,:,:,:),[4 3 2 1]); 110 | end 111 | 112 | %% Find any shell strain lines 113 | strnStarts = regexp(inStr,' strains \(.*?\r\n\r\n','once'); 114 | if strnStarts 115 | % Fetch strain headers 116 | strainHdrs = matlab.lang.makeValidName(strsplit(strtrim(... 117 | inStr(strnStarts+(20:100))),' ')); 118 | nChannels = length(strainHdrs); 119 | 120 | % Build the string format per element 121 | nChElemLine = 16 + nChNL; 122 | nChIPLine = 89 + nChNL; 123 | nChElem = nChElemLine + nIP*nChIPLine; 124 | fmtChannels = repmat(' %f',1,nChannels); 125 | fmtIPline = ['%*s ipt' fmtChannels]; 126 | fmtElem = ['%*u- %*u ' repmat(fmtIPline,1,nIP)]; 127 | offset = strnStarts - timestepInds(1) + 244; 128 | 129 | % Gather strains as channels-by-IP-by-elem-by-time array 130 | strainData = zeros(nChannels, nIP, nElems, nTimesteps,'single'); 131 | for i = 1:nTimesteps 132 | tmpStr = inStr(timestepInds(i) + offset + (1:nChElem*nElems)); 133 | strainData(:,:,:,i) = reshape(sscanf(tmpStr,fmtElem),nChannels,nIP,nElems); 134 | end 135 | 136 | % Unpack strains to shell data table 137 | for hdrNo = 1:length(strainHdrs) 138 | this.SHELL_DATA.(strainHdrs{hdrNo}) = permute(strainData(hdrNo,:,:,:),[4 3 2 1]); 139 | end 140 | end 141 | 142 | %% Find BEAM elements 143 | 144 | % Pick out every beam number line 145 | beamPattern = '^ beam/truss # =\s*(\d+)\s*part ID =\s*(\d+)\s*material type=\s*(\d+)\s*$'; 146 | [ba,bb,~,~,be] = regexp(inStr,beamPattern,'lineanchors'); 147 | if ~isempty(ba) 148 | be = cat(1,be{:}); 149 | 150 | % Extract an index of the element numbers and the part/mat they have 151 | allBeamElNos = str2num(char(be(:,1))); %#ok 152 | [unqBeamElNos,unqBeam1sts,beamNoGrps] = unique(allBeamElNos,'stable'); 153 | unqBeamElPartMatNos = [unqBeamElNos cellfun(@str2double,be(unqBeam1sts,2:3))]; 154 | 155 | 156 | % Counts of the variables output from beams 157 | nTimesteps = length(timestepArr); 158 | nBeamRes = 6; 159 | nElems = length(unqBeamElNos); 160 | 161 | % Which timestep was each beam line belonging to? 162 | beamTstepGrp = interp1(timestepInds,1:length(timestepInds),ba(:),'previous','extrap'); 163 | 164 | % We know the pattern of resultants immediately after beam numbers, so we 165 | % can just extract that text directly via an offset index: 166 | % % resultants axial shear-s shear-t moment-s moment-t torsion 167 | % % -1.622E-01 1.623E-01 1.564E+01 -4.023E-02 -1.297E-04 2.992E-03 168 | % 169 | C = str2num(inStr(bsxfun(@plus,(90:157),bb(:)))); %#ok 170 | 171 | % Make a NaN matrix in case some elements were only output at certain 172 | % timesteps 173 | beamEloutMat = nan(nTimesteps, nBeamRes, nElems); 174 | for i = 1:size(C,1) 175 | beamEloutMat(beamTstepGrp(i),:,beamNoGrps(i)) = C(i,:); 176 | end 177 | 178 | % Fetch beam element output names 179 | [~,~,~,~,beamResNames] = regexp(inStr,' resultants(\s+([^\s+]+))+\s*$','match','once','lineanchors'); 180 | beamResNames = matlab.lang.makeValidName(strsplit(strtrim(beamResNames{1}),' ')); 181 | 182 | this.BEAM_ELEM_INFO = array2table(unqBeamElPartMatNos,'Var',{'ELEM_ID','PART_ID','MAT_ID'}); 183 | beamT = array2table(timestepArr(:),'Var',{'time'}); 184 | for t = 1:length(beamResNames) 185 | beamT.(beamResNames{t}) = permute(beamEloutMat(:,t,:),[1 3 2]); 186 | end 187 | this.BEAM_DATA = beamT; 188 | end 189 | end 190 | end 191 | end -------------------------------------------------------------------------------- /+lsdyna/simulation.m: -------------------------------------------------------------------------------- 1 | classdef simulation < handle 2 | %lsdyna.simulation Run one or more LS-Dyna simulations from MATLAB 3 | % 4 | % 5 | % Basic usage (run one simulation): 6 | % S = lsdyna.simulation('C:\FolderToSim\mainFile.k') 7 | % S.run 8 | % 9 | % 10 | % Multiple simulations (in series): 11 | % baseFolder = 'C:\FolderToSims'; 12 | % for i = 1:10 13 | % simFolder = fullfile(baseFolder,sprintf('sim%d',i)); 14 | % S(i) = lsdyna.simulation(fullfile(simFolder,'mainFile.k')); 15 | % end 16 | % S.run % Each simulation will be run, one after the other 17 | % 18 | % 19 | % Multiple simulations (in parallel): 20 | % baseFolder = 'C:\FolderToSims'; 21 | % for i = 1:10 22 | % simFolder = fullfile(baseFolder,sprintf('sim%d',i)); 23 | % S(i) = lsdyna.simulation(fullfile(simFolder,'mainFile.k')); 24 | % S(i).cmdBlocking = false; 25 | % end 26 | % % Run simulations in parallel using 4 threads. The first 4 27 | % % simulations will start in a new command window, and when each is 28 | % % complete, it will fire the next simulation to run in the available 29 | % % thread. 30 | % S.run('threads',4) 31 | % 32 | % Note: 33 | % Listeners can be added to the following events for a simulation in 34 | % order to automatically fire cleanup/read functions: 35 | % SimProcComplete Event fired when the dyna.exe command finishes 36 | % SimProcStarting Event fired when dyna.exe command is starting 37 | % SimTermination Event fired when termination status is known/read 38 | 39 | 40 | properties (Transient) 41 | folder % Input folder for simulation 42 | inputFile % Main .k or .key file to be executed 43 | end 44 | properties (Hidden) 45 | cmdProcessId % Windows processId for the command line window running dyna.exe 46 | cmdNonBlockingListener % Storage for listener object that runs when running dyna asynchronously 47 | cmdStartedTime % Storage for the timestamp when executable called 48 | cmdLastCheckedTime % Storage for the timestamp when executable last checked for completion 49 | end 50 | properties 51 | cmdBlocking = true; % Set to false to run dyna as separate asynchronous process 52 | cmdPollingPeriod = 2; % Seconds between checks for Dyna .exe completion 53 | cmdTimeoutDuration = minutes(inf); 54 | terminationStatus = 'Unknown' % Error / Normal / Unknown / Timeout 55 | terminationTime % Timestamp of simulation completion 56 | messagContents % Contents of output "messag" file 57 | end 58 | 59 | properties (Constant, Hidden) 60 | dynaExe = 'C:\LSDYNA\program\ls-dyna_smp_s_R610_winx64_ifort101.exe' 61 | end 62 | properties (Dependent, Hidden) 63 | asciiFullfile 64 | storageMatFullfile 65 | end 66 | properties 67 | PreSimCallback % Callback called before simulation is run 68 | end 69 | events 70 | SimProcStarting % Event fired when dyna.exe command is starting 71 | SimProcComplete % Event fired when the dyna.exe command finishes 72 | SimTermination % Event fired when termination status is known/read 73 | end 74 | methods %% Utility methods 75 | function runInThreads(this,varargin) 76 | IP = inputParser; 77 | IP.addParameter('threads', 4) 78 | IP.parse(varargin{:}) 79 | opts = IP.Results; 80 | 81 | lstnrCell = cell(1,opts.threads); 82 | 83 | nSims = numel(this); 84 | currSimNo = 0; 85 | 86 | % Start N threads 87 | for i = 1:opts.threads 88 | runNextSim() 89 | end 90 | 91 | function runNextSim() 92 | currSimNo = currSimNo + 1; 93 | if currSimNo > nSims 94 | return; 95 | end 96 | nextCellNo = find(cellfun(@isempty, lstnrCell),1); 97 | runSimNoInCellNo(currSimNo,nextCellNo) 98 | end 99 | function runSimNoInCellNo(simNo,slotNo) 100 | % Listen for the termination of this sim, and go! 101 | lstnrCell{slotNo} = addlistener(... 102 | this(simNo),'SimTermination',@(~,~)cleanupLstnrNo(slotNo,simNo)); 103 | fprintf('Sim %d of %d [slot #%d]: Starting ...\n', simNo, nSims, slotNo) 104 | this(simNo).run 105 | end 106 | function cleanupLstnrNo(lstnrNo,simNo) 107 | % Delete the old listener and clear out a spot in the queue 108 | fprintf('Sim %d of %d [slot #%d]: Ended.\n', simNo, nSims, lstnrNo) 109 | delete(lstnrCell{lstnrNo}) 110 | lstnrCell{lstnrNo} = []; 111 | runNextSim() 112 | end 113 | 114 | end 115 | function run(this,varargin) 116 | % sim.run 117 | % sims.run('threads',4) 118 | 119 | %% Handle multiple sims 120 | if numel(this)>1 && any(~[this.cmdBlocking]) 121 | this.runInThreads(varargin{:}) 122 | return; 123 | end 124 | 125 | %% Call for any pre-simulation code to be run 126 | if ~isempty(this.PreSimCallback) 127 | this.PreSimCallback(); 128 | end 129 | 130 | %% 131 | baseExecStr = sprintf('cd /d "%s" & %s I=%s O=d3hsp',... 132 | this.folder, this.dynaExe, this.inputFile); 133 | this.systemCheck 134 | if this.cmdBlocking 135 | execStr = baseExecStr; 136 | else 137 | % We want to spawn a new non-blocking process 138 | execStr = [baseExecStr ' & ']; 139 | % But we need to keep track of this new process. First, 140 | % gather any old processes that may have been spawned 141 | thispid = feature('getpid'); 142 | wmicOut = 'commandline,processid'; 143 | wmicStr = sprintf(['wmic process where ' ... 144 | '(name="%s" and parentprocessid="%d" ' ... 145 | 'and commandline like "%%%s%%" and commandline like "%%%s%%") get %s'],... 146 | 'cmd.exe', thispid, strrep(this.folder,'\','\\'),... 147 | strrep(this.inputFile,'\','\\'), wmicOut); 148 | [~,pList] = system(wmicStr); 149 | wmicReturns = strsplit(strtrim(pList),'\n')'; 150 | % ProcessIds of pre-existing cmd windows 151 | oldChildProcStrs = regexp(wmicReturns(2:end),'\d+$','match','once'); 152 | end 153 | 154 | % Run the actual DYNA simulation! 155 | notify(this,'SimProcStarting') 156 | this.cmdStartedTime = datetime; 157 | status = system(execStr); 158 | if status>1 159 | warning('Execution of LS-Dyna command failed!') 160 | end 161 | 162 | % Handle a running or finished command line 163 | if this.cmdBlocking 164 | % The cmd.exe process has complete! 165 | notify(this,'SimProcComplete'); 166 | else 167 | % We can try to identify the cmd.exe process we just made 168 | [~,pList] = system(wmicStr); 169 | wmicReturns = strsplit(strtrim(pList),'\n')'; 170 | % ProcessIds of pre-existing cmd windows 171 | currChildProcStrs = regexp(wmicReturns(2:end),'\d+$','match','once'); 172 | newChildProcStrs = setdiff(currChildProcStrs, oldChildProcStrs); 173 | 174 | if isempty(newChildProcStrs) 175 | warning('Could not detect spawned LS-Dyna executing process') 176 | elseif length(newChildProcStrs)==1 177 | % Found it! 178 | this.cmdProcessId = newChildProcStrs{1}; 179 | fprintf('LS-Dyna called within cmd.exe (procid %s) spawned by MATLAB (procid %d)\n', this.cmdProcessId, thispid) 180 | fprintf('Simulation termination will be checked at %gs intervals.\n', this.cmdPollingPeriod) 181 | % Listen to a sim completion event to close cmd window 182 | delete(this.cmdNonBlockingListener) 183 | this.cmdNonBlockingListener = addlistener(this,'SimProcComplete',... 184 | @(src,evnt)system(sprintf(... 185 | 'wmic process where processid="%s" call terminate',... 186 | this.cmdProcessId))); 187 | % Use a timer to poll for the dyna exe process status, 188 | % and fire an event when that timer is closed 189 | t = timer; 190 | t.TimerFcn = @(timerObj,~)this.pollSystemExe(timerObj); 191 | t.StopFcn = @(obj,evnt)notify(this,'SimProcComplete'); 192 | t.Period = this.cmdPollingPeriod; 193 | t.ExecutionMode = 'fixedSpacing'; 194 | start(t) 195 | 196 | else 197 | warning('Multiple new LS-Dyna executing processes detected!') 198 | end 199 | end 200 | end 201 | 202 | function fetchSimulationStatus(this, fireTerminationEvent) 203 | % Hunt through the messag file for termination status 204 | msgFileStr = fullfile(this.folder,'messag'); 205 | if exist(msgFileStr,'file') 206 | this.messagContents = fileread(msgFileStr); 207 | [~,~,~,~,termParts] = regexp(this.messagContents,'^ ([\w ]*) t e r m i n a t i o n \s+(.{17})','lineanchors','match','once'); 208 | if ~isempty(termParts) 209 | this.terminationStatus = strrep(termParts{1},' ',''); 210 | this.terminationTime = datetime(termParts{2},'InputFormat','MM/dd/uuuu HH:mm:ss'); 211 | else 212 | warning('No termination information found in %s',msgFileStr) 213 | end 214 | else 215 | warning('No messag file was found in %s',this.folder) 216 | end 217 | if nargin>1 && fireTerminationEvent 218 | notify(this,'SimTermination') 219 | end 220 | end 221 | 222 | function pollSystemExe(this,timerObj) 223 | % Utility to check unblocked dyna process for completion 224 | [~,wmicOutput] = system(sprintf(... 225 | 'wmic process where (parentprocessid="%s" and executablepath="%s") get commandline,processid',... 226 | this.cmdProcessId, strrep(this.dynaExe,'\','\\'))); 227 | wmicOutputs = strsplit(strtrim(wmicOutput),'\n')'; 228 | if numel(wmicOutputs)==2 229 | % Do nothing - simulation still running 230 | this.cmdLastCheckedTime = datetime; 231 | % Check if we should be timing out 232 | if isfinite(this.cmdTimeoutDuration) 233 | durationRunning = this.cmdLastCheckedTime - this.cmdStartedTime; 234 | if durationRunning > this.cmdTimeoutDuration 235 | warning('%s: Timeout (%s) reached - killing simulation...: %s\n',datestr(now),char(this.cmdTimeoutDuration),this.folder) 236 | stop(timerObj) 237 | delete(timerObj) 238 | notify(this,'SimProcComplete') 239 | end 240 | end 241 | elseif numel(wmicOutputs) < 2 242 | % There is no child dyna process of our registered cmd.exe 243 | % process. Presume it has now finished. 244 | fprintf('%s: Asynchronous simulation complete: %s\n',datestr(now),this.folder) 245 | if nargin>1 246 | stop(timerObj) 247 | delete(timerObj) 248 | end 249 | else 250 | warning('Unexpected number of child processes found') 251 | end 252 | end 253 | function systemCheck(this) 254 | % Check that we've got a dyna executable to run 255 | if ~exist(this.dynaExe,'file') 256 | error('LS-Dyna executable %s not found',this.dynaExe) 257 | end 258 | 259 | % Holy crap this one was tough to track down. There's a 260 | % potentially troublesome way that MATLAB sets up environment 261 | % variables when it opens a new system cmd window. Cheers to 262 | % James Kennedy from the Dyna User's Group for helping to 263 | % identify this issue. 264 | oldEnv = getenv('KMP_STACKSIZE'); 265 | if ~isempty(regexp(oldEnv,'[^0-9]','once')) 266 | newEnv = strrep(strrep(oldEnv,'k','000'),'m','000000'); 267 | setenv('KMP_STACKSIZE',newEnv) 268 | warning('Incompatible KMP_STACKSIZE environment variable (%s) changed to: %s',... 269 | oldEnv, newEnv) 270 | end 271 | end 272 | end 273 | methods 274 | % Constructor 275 | function this = simulation(inputFolder,varargin) 276 | % Accept a directory (or file) to look for 277 | if exist(inputFolder,'dir') 278 | this.folder = inputFolder; 279 | % Attempt to find the main k or key file 280 | kFiles = dir(fullfile(this.folder,'*.k*')); 281 | if numel(kFiles)==1 282 | this.inputFile = kFiles.name; 283 | end 284 | elseif exist(inputFolder,'file') 285 | [this.folder,b,c] = fileparts(inputFolder); 286 | this.inputFile = [b c]; 287 | else 288 | warning('Input file/folder (%s) does not exist!', inputFolder) 289 | end 290 | 291 | % Specify basic Norm/Err status when a sim process completes 292 | addlistener(this,'SimProcComplete',@(src,~)src.fetchSimulationStatus(true)); 293 | addlistener(this,'SimTermination',@(src,~)... 294 | fprintf('Simulation complete with %s termination (%s): %s\n',... 295 | src.terminationStatus,datestr(src.terminationTime),src.folder)); 296 | end 297 | end 298 | end --------------------------------------------------------------------------------