Home > matpower6.0 > most > apply_profile.m

apply_profile

PURPOSE ^

APPLY_PROFILE Applies changes defined in a profile to a data structure.

SYNOPSIS ^

function argout = apply_profile(profile, argin, dim)

DESCRIPTION ^

APPLY_PROFILE  Applies changes defined in a profile to a data structure.

   CHGTABS = APPLY_PROFILE( PROFILE, CHGTABSI )
   XGD     = APPLY_PROFILE( PROFILE, XGDI, DIM )
   SD      = APPLY_PROFILE( PROFILE, SDI, DIM )
   CTSETS  = APPLY_PROFILE( PROFILE, CTSETS, DIM ) (not yet implemented)
   
   Applies a single profile of the given type to the given ARGIN. There are
   4 different types of profiles, and each one affects differently the
   input to produce the output. Profile input must contain the following
   fields:

   Inputs:
       PROFILE: a single-dimensional Profile struct with the following fields:
           .type       (string)
           .table      (string or scalar)
           .rows       (vector)
           .col        (scalar)
           .chgtype    (scalar)
           .values     (array) with at most 3 dimensions assumed to be
                         [ (1 or nt) by (1 or nj_max) or (1 or length(rows)) ]
           See IDX_PROFILE for details on the Profile struct.

       CHGTABI:    cell array of change tables to be modified
       XGDI:       xGenData struct to be modified
       STORAGEI:   StorageData struct to be modified
       CTSETSI:    array with ContingencyData (not yet implemented)

       DIM :   (scalar) indicates the total number of elements in the
               table or field being modified. Elements here refers to the
               3rd dimension, not time nor scenarios, but rather elements
               such as generators, number of different contingencies in
               master chgtab matrix (different labels), and storage units.
               DIM required to be able to expand, to a full DIM dimension,
               the data to be modified when it is summarized by a
               singleton dimension representing all the elements in that
               particular data set. It is ignored for type 'mpcData',
               mandatory for all other types.

   Outputs:
       CHGTABS : cell array of modified change tables (nr x 7)
       XGD:      modified xGenData struct
       STORAGE:  modified StorageData struct
       CTSETS:   (not yet implemented)

   Additional notes:

   In general, field 'values' does not need to match dimensions of
   dim = [nt nj_max n], where 'n' represents the subset of elements being
   affected by the profile, i.e., the elements indicated by 'rows', but it
   does need to be smaller or equal. Each dimension of 'values' is allowed
   to be either the indicated above, or a singleton dimension, in which
   case a singleton meaning that the profile "applies to all" elements in
   that dimension, with the exception that the third dimension may be a
   singleton also in the case affecting a single element (as opposed to all
   elements in the third dimension).

   type == 'mpcData'
       Generates/adds contingency-like tables to a cell array can be
       used to apply a change 'chgtype' to values in column 'col' of
       elements 'rows' on table 'table'. 'values' is a numeric array
       with up to 3 dimensions organized necessarily as in [nt nj_max
       n]. The third dimension indicates the subset of elements to
       which the profile is to be applied. Output CHGTABS is a (nt by
       nj) cell array of chgtab matrices (7 cols) with unspecified
       labels nor probabilities. CHGTABI must always be provided, even
       if it's a cell array with (nt x nj_max) empty entries. These
       dimensions are required in order to be able to expand changes
       correctly across time periods and scenarios. Dimensions of
       'values' are expanded if required (i.e., if inconsistent with nt,
       nj_max, or length of 'rows', resp.).

   type == 'xGenData'
       Profile modifies the field of XGD struct indicated by the
       string 'table'. 'rows' indicates gens to modify, 'col' is
       ignored, and 'chgtype' the type of change using 'values'.
       Dimensions of 'values' are expanded if required (ie, if
       inconsistent with nt, nj_max, or length of 'rows', resp.).

   type == 'StorageData'
       Profile modifies the field of STORAGE struct indicated by the
       string 'table'. 'rows' indicates storage units to modify (using
       storage-dedicated idx's as opposed to gen idx's), 'col' is ignored,
       and 'chgtype' the type of change using 'values'. Dimensions of
       'values' are expanded if required (ie, if inconsistent with nt,
       nj_max, or length of 'rows', resp.).

   type == 'ContingencyData' (not yet implemented)
       Profile modifies the provided 'indicative' 3-dim array of
       binary variables: 1st dim spans the labels of contingencies,
       2nd dim spans time periods, and 3rd dim spans scenarios. Thus,
       'rows' indicates which labeled contingencies are to be modified
       by the profile, 'col' is ignored, and 'cghtype' the type of
       change using 'values'. Dimensions of 'values' expanded if
       required  (ie, if inconsistent with nt, nj_max, or length of
       'rows', resp.).

CROSS-REFERENCE INFORMATION ^

This function calls: This function is called by:

SOURCE CODE ^

0001 function argout = apply_profile(profile, argin, dim)
0002 %APPLY_PROFILE  Applies changes defined in a profile to a data structure.
0003 %
0004 %   CHGTABS = APPLY_PROFILE( PROFILE, CHGTABSI )
0005 %   XGD     = APPLY_PROFILE( PROFILE, XGDI, DIM )
0006 %   SD      = APPLY_PROFILE( PROFILE, SDI, DIM )
0007 %   CTSETS  = APPLY_PROFILE( PROFILE, CTSETS, DIM ) (not yet implemented)
0008 %
0009 %   Applies a single profile of the given type to the given ARGIN. There are
0010 %   4 different types of profiles, and each one affects differently the
0011 %   input to produce the output. Profile input must contain the following
0012 %   fields:
0013 %
0014 %   Inputs:
0015 %       PROFILE: a single-dimensional Profile struct with the following fields:
0016 %           .type       (string)
0017 %           .table      (string or scalar)
0018 %           .rows       (vector)
0019 %           .col        (scalar)
0020 %           .chgtype    (scalar)
0021 %           .values     (array) with at most 3 dimensions assumed to be
0022 %                         [ (1 or nt) by (1 or nj_max) or (1 or length(rows)) ]
0023 %           See IDX_PROFILE for details on the Profile struct.
0024 %
0025 %       CHGTABI:    cell array of change tables to be modified
0026 %       XGDI:       xGenData struct to be modified
0027 %       STORAGEI:   StorageData struct to be modified
0028 %       CTSETSI:    array with ContingencyData (not yet implemented)
0029 %
0030 %       DIM :   (scalar) indicates the total number of elements in the
0031 %               table or field being modified. Elements here refers to the
0032 %               3rd dimension, not time nor scenarios, but rather elements
0033 %               such as generators, number of different contingencies in
0034 %               master chgtab matrix (different labels), and storage units.
0035 %               DIM required to be able to expand, to a full DIM dimension,
0036 %               the data to be modified when it is summarized by a
0037 %               singleton dimension representing all the elements in that
0038 %               particular data set. It is ignored for type 'mpcData',
0039 %               mandatory for all other types.
0040 %
0041 %   Outputs:
0042 %       CHGTABS : cell array of modified change tables (nr x 7)
0043 %       XGD:      modified xGenData struct
0044 %       STORAGE:  modified StorageData struct
0045 %       CTSETS:   (not yet implemented)
0046 %
0047 %   Additional notes:
0048 %
0049 %   In general, field 'values' does not need to match dimensions of
0050 %   dim = [nt nj_max n], where 'n' represents the subset of elements being
0051 %   affected by the profile, i.e., the elements indicated by 'rows', but it
0052 %   does need to be smaller or equal. Each dimension of 'values' is allowed
0053 %   to be either the indicated above, or a singleton dimension, in which
0054 %   case a singleton meaning that the profile "applies to all" elements in
0055 %   that dimension, with the exception that the third dimension may be a
0056 %   singleton also in the case affecting a single element (as opposed to all
0057 %   elements in the third dimension).
0058 %
0059 %   type == 'mpcData'
0060 %       Generates/adds contingency-like tables to a cell array can be
0061 %       used to apply a change 'chgtype' to values in column 'col' of
0062 %       elements 'rows' on table 'table'. 'values' is a numeric array
0063 %       with up to 3 dimensions organized necessarily as in [nt nj_max
0064 %       n]. The third dimension indicates the subset of elements to
0065 %       which the profile is to be applied. Output CHGTABS is a (nt by
0066 %       nj) cell array of chgtab matrices (7 cols) with unspecified
0067 %       labels nor probabilities. CHGTABI must always be provided, even
0068 %       if it's a cell array with (nt x nj_max) empty entries. These
0069 %       dimensions are required in order to be able to expand changes
0070 %       correctly across time periods and scenarios. Dimensions of
0071 %       'values' are expanded if required (i.e., if inconsistent with nt,
0072 %       nj_max, or length of 'rows', resp.).
0073 %
0074 %   type == 'xGenData'
0075 %       Profile modifies the field of XGD struct indicated by the
0076 %       string 'table'. 'rows' indicates gens to modify, 'col' is
0077 %       ignored, and 'chgtype' the type of change using 'values'.
0078 %       Dimensions of 'values' are expanded if required (ie, if
0079 %       inconsistent with nt, nj_max, or length of 'rows', resp.).
0080 %
0081 %   type == 'StorageData'
0082 %       Profile modifies the field of STORAGE struct indicated by the
0083 %       string 'table'. 'rows' indicates storage units to modify (using
0084 %       storage-dedicated idx's as opposed to gen idx's), 'col' is ignored,
0085 %       and 'chgtype' the type of change using 'values'. Dimensions of
0086 %       'values' are expanded if required (ie, if inconsistent with nt,
0087 %       nj_max, or length of 'rows', resp.).
0088 %
0089 %   type == 'ContingencyData' (not yet implemented)
0090 %       Profile modifies the provided 'indicative' 3-dim array of
0091 %       binary variables: 1st dim spans the labels of contingencies,
0092 %       2nd dim spans time periods, and 3rd dim spans scenarios. Thus,
0093 %       'rows' indicates which labeled contingencies are to be modified
0094 %       by the profile, 'col' is ignored, and 'cghtype' the type of
0095 %       change using 'values'. Dimensions of 'values' expanded if
0096 %       required  (ie, if inconsistent with nt, nj_max, or length of
0097 %       'rows', resp.).
0098 
0099 % Created by Daniel Munoz-Alvarez (4/18/2013)
0100 
0101 %   MOST
0102 %   Copyright (c) 2013-2016, Power Systems Engineering Research Center (PSERC)
0103 %   by Daniel Munoz-Alvarez, PSERC Cornell
0104 %
0105 %   This file is part of MOST.
0106 %   Covered by the 3-clause BSD License (see LICENSE file for details).
0107 %   See http://www.pserc.cornell.edu/matpower/ for more info.
0108 
0109 if nargin < 2
0110     error('apply_profile: insufficient arguments')
0111 end
0112 
0113 % (A) Preliminary checking
0114 
0115 typ     = profile.type;
0116 tbl     = profile.table;
0117 rows    = profile.rows;
0118 col     = profile.col;
0119 chgtyp  = profile.chgtype;
0120 val     = profile.values;
0121 
0122 [PR_REP, PR_REL, PR_ADD, PR_TCONT, PR_TYPES, PR_TMPCD,...
0123     PR_TXGD, PR_TCTD, PR_TSTGD, PR_CHGTYPES] = idx_profile;
0124 
0125 if length(profile)~=1
0126     error('apply_profile: must input a single profile')
0127 end
0128 if length(rows) > 1 && any(rows == 0)
0129     error('apply_profile: rows field of a profile must not contain zero unless it is the only entry')
0130 end
0131     
0132 
0133 switch typ
0134 % (B) Type mpcData profile
0135     case 'mpcData'
0136         chgtabs = argin;
0137 
0138         nt = size(chgtabs, 1);
0139         nj_max = size(chgtabs, 2);
0140 
0141 % (B.1) Check dimensions and fields of PROFILE
0142         if length(profile) > 1
0143             error('apply_profile: multiple profiles should be added separately')
0144         end
0145 
0146         % (B) Check consistency of IDX, VALUES and CHGTABI
0147         if size(val,3) ~= length(rows)
0148             error('apply_profile: third dimension of profile.values should match length of profile.rows')
0149         end
0150 
0151         if isempty(chgtabs)
0152             error('apply_profile: chgtabs cell array should have dimensions nt by nj_max')
0153         end
0154 
0155 
0156         % (C) Generate contingency-like rows to add to CHGTABI
0157         
0158 %       At this point, val can only have dimensions (1 or nt) by
0159 %       (1 or nj_max) by (1 or length(rows)), so before transforming into a
0160 %       chgtab, val needs to be expanded to full dimensions [nt nj_max length(rows)]
0161         
0162         if size(val,1) == 1 && nt > 1
0163             val = repmat(val, [nt 1 1]);
0164         end
0165         if size(val,2) == 1 && nj_max > 1
0166             val = repmat(val, [1 nj_max 1]);
0167         end
0168         if size(val,3) == 1 && length(rows) > 1
0169             val = repmat(val, [1 1 length(rows)]);
0170         end
0171         
0172         if any(tbl == PR_TMPCD)
0173             for t = 1:nt
0174                 for j = 1:nj_max
0175                     new_rows = [];
0176                     for i = 1:length(rows)
0177                             new_rows = [ new_rows ; 0 0 tbl rows(i) col chgtyp val(t,j,i) ];
0178                     end
0179                     chgtabs{t,j} = [ chgtabs{t,j} ; new_rows ];
0180                 end
0181             end
0182         else
0183             error('apply_profile: indicated profile.table not supported for profile changes')
0184         end
0185 
0186         argout = chgtabs;
0187     
0188     case 'xGenData'
0189 % (C) Type xGenData profile
0190         xgd = argin;
0191         ng = dim;
0192         
0193         nt_adhoc = max( size(val,1), size(xgd.(tbl),2) ); % nt_adhoc equals 1 or nt
0194         nj_adhoc = max( size(val,2), size(xgd.(tbl),3) ); % nj_adhoc equals 1 or nj_max
0195 
0196 % (C.1) Check fields of PROFILE
0197         if ~ischar(tbl)
0198             error('apply_profile: table field of xGenData profile must be a string')
0199         end
0200         if ~any(strcmp(tbl, PR_TXGD))
0201             error('apply_profile: field %s of xgd struct cannot be modified through a profile',tbl)
0202         end
0203 
0204 % (C.2) Check consistency of ROWS, VALUES and affected field of XGD
0205 
0206         if ~(size(xgd.(tbl),1) == 1 || size(xgd.(tbl),1) == ng)
0207              error('apply_profile: rows of xgd.%s must equal 1 or ng',tbl)
0208         end
0209         if ~(size(xgd.(tbl),2) == 1 || size(xgd.(tbl),2) == nt_adhoc)
0210             error('apply_profile: time dimension mismatch between xgd.%s and profile.values',tbl)
0211         end
0212         if ~(size(xgd.(tbl),3) == 1 || size(xgd.(tbl),3) == nj_adhoc)
0213             error('apply_profile: scenario dimension mismatch between xgd.%s and profile.values',tbl)
0214         end
0215         
0216         
0217         if ~(size(val,1) == 1 || size(val,1) == nt_adhoc)
0218             error('apply_profile: time dimension mismatch between xgd.%s and profile.values',tbl)
0219         end
0220         if ~(size(val,2) == 1 || size(val,2) == nj_adhoc)
0221             error('apply_profile: scenario dimension mismatch between xgd.%s and profile.values',tbl)
0222         end
0223         if length(rows) == 1
0224             if size(val,3) ~= 1
0225                 error('apply_profile: 3rd dimension of values field must equal 1 when rows is scalar')
0226             end
0227         else
0228             if ~(size(val,3) == length(rows) || size(val,3) == 1)
0229                 error('apply_profile: 3er dimension of values field must equal 1 or length of rows field when rows is not scalar')
0230             end
0231         end
0232 
0233         
0234 % (C.3) Verify validity of changes and expand field in question to full dimensions if required
0235 %       Important: Notice the permutation of values dimensions from
0236 %       [1 2 3] to [3 1 2].
0237 
0238           val = permute(val,[3 1 2]);
0239           
0240           % From here and on, val dimensions are
0241           % (1 or legnth(rows)) by (1 or nt) by (1 or nj)
0242           % Also, xgd.(tbl) dimensions are
0243           % (1 or ng) by (1 or nt) by (1 or nj)
0244 
0245 
0246           % Expand cols (time) of xgd.(tbl)
0247           % if val has a time dimension and xgd.(tbl) does not (only necessary if nt_adhoc > 1)
0248           if size(val,2) > size(xgd.(tbl),2)
0249               xgd.(tbl) = repmat( xgd.(tbl), [1 size(val,2) 1]);
0250           
0251           % Expands cols (time) of val to match xgd.(tbl) time dimension
0252           elseif size(xgd.(tbl),2) > size(val,2)
0253               val = repmat( val, [ 1 nt_adhoc 1]);
0254           end
0255           
0256           % Expand 3rd dim (scenarios) of xgd.(tbl)
0257           % if val has a scenario dimension and xgd.(tbl) does not (only necessary if nj_adhoc > 1)
0258           if size(val,3) > size(xgd.(tbl),3)
0259               xgd.(tbl) = repmat( xgd.(tbl), [ 1 1 size(val,3)] ) ;
0260           
0261           % Expands 3rd dim (scenarios) of val to match xgd.(tbl) scenarios dimension
0262           elseif size(xgd.(tbl),3) > size(val,3)
0263               val = repmat(val, [ 1 1 size(xgd.(tbl),3)]);
0264           end
0265 
0266           % Expand rows (gens) of xgd.(tbl)
0267           % if profile modifies subset of gens (i.e. if rows~=0)
0268           if size(xgd.(tbl),1) == 1 && any(rows ~= 0)
0269               xgd.(tbl) = repmat( xgd.(tbl), [ ng 1 1]);
0270           end
0271 
0272           % Expand rows of val to match gens to modify
0273           if size(val,1) == 1 && size(xgd.(tbl), 1) == ng && length(rows) > 1
0274               val = repmat(val, [length(rows) 1 1]);
0275           elseif size(val,1) == 1 && size(xgd.(tbl), 1) == ng && length(rows) == 1 && rows == 0
0276               val = repmat(val, [ng 1 1]);
0277           end
0278 
0279           % Error if chgtype is CT_REL or CT_ADD and field involved is empty
0280           if (chgtyp == PR_REL || chgtyp == PR_ADD) && isempty(xgd.(tbl))
0281               error('apply_profile: PR_REL or PR_ADD modification cannot be done to xgd.%s if it is empty',tbl)
0282           end
0283 
0284 % (C.4) Apply change
0285           if length(rows) == 1 && rows == 0                 %% modify all rows
0286             if chgtyp == PR_REP                                    %% replace
0287               xgd.(tbl) = val;
0288             elseif chgtyp == PR_REL                                %% scale
0289               xgd.(tbl) = val .* xgd.(tbl);
0290             elseif chgtyp == PR_ADD                                %% shift
0291               xgd.(tbl) = val + xgd.(tbl);
0292             else
0293               error('apply_profile: modification type %d for xgd table not supported', chgtyp);
0294             end
0295           else                                              %% modify single row
0296             if chgtyp == PR_REP                                    %% replace
0297               xgd.(tbl)(rows,:,:) = val;
0298             elseif chgtyp == PR_REL                                %% scale
0299               xgd.(tbl)(rows,:,:) = val .* xgd.(tbl)(rows,:,:);
0300             elseif chgtyp == PR_ADD                                %% shift
0301               xgd.(tbl)(rows,:,:) = val + xgd.(tbl)(rows,:,:);
0302             else
0303               error('apply_profile: modification type %d for xgd table not supported', chgtyp);
0304             end
0305           end
0306 
0307         
0308         argout = xgd;
0309 
0310     case 'ContingencyData'
0311 % (D) Type ContingencyData profile (not yet implemented)
0312         ct_subset = argin;
0313         error('apply_profile: type ContingencyData is not yet supported')
0314         argout = ct_subset;
0315 
0316     case 'StorageData'
0317 % (E) Type StorageData profile
0318         storage = argin;
0319         ns = dim;   % total number of storage units in the system
0320         nt_adhoc = max( size(val,1), size(storage.(tbl),2) ); % nt_adhoc equals 1 or nt
0321 
0322 % (E.1) Check fields of PROFILE
0323         if ~ischar(tbl)
0324             error('apply_profile: table field of storage profile must be a string')
0325         end
0326         if ~any(strcmp(tbl, PR_TSTGD))
0327             error('apply_profile: field %s of storage struct cannot be modified through a profile',tbl)
0328         end
0329 
0330 % (E.2) Check consistency of ROWS, VALUES and affected field of STORAGE
0331         
0332         if ~(size(storage.(tbl),1) == 1 || size(storage.(tbl),1) == ns)
0333              error('apply_profile: first dimension of field %s of storage struct must equal 1 or ns',tbl)
0334         end
0335         if ~(size(storage.(tbl),2) == 1 || size(storage.(tbl),2) == nt_adhoc)
0336             error('apply_profile: time dimension mismatch between storage.%s and profile.values',tbl)
0337         end
0338         if size(storage.(tbl),3) ~= 1
0339             error('apply_profile: no scenario dimension (3rd) allowed for storage.%s field',tbl)
0340         end
0341         
0342         
0343         if ~(size(val,1) == 1 || size(val,1) == nt_adhoc)
0344             error('apply_profile: time dimension mismatch between storage.%s and profile.values',tbl)
0345         end
0346         if size(val,2) ~= 1
0347             error('apply_profile: 2nd dimension of values field must equal 1 since no scenario dependent changes allowed ')
0348         end
0349         if length(rows) == 1
0350             if size(val,3) ~= 1
0351                 error('apply_profile: 3rd dimension of values field must equal 1 when rows is scalar')
0352             end
0353         else
0354             if ~(size(val,3) == length(rows) || size(val,3) == 1)
0355                 error('apply_profile: 3er dimension of values field must equal 1 or length of rows field when rows is a vector')
0356             end
0357         end
0358 
0359         
0360 % (E.3) Verify validity of changes and expand field in question to full dimensions if required
0361 %       Important: Notice the permutation of values dimensions from
0362 %       [1 2 3] to [3 1 2]. No scenario dimension allowed.
0363 
0364           val = permute(val,[3 1 2]); % Squeeze not use to avoid problems when nt=1
0365           val = val(:,:,1); % From here and on, val dimensions are (1 or legnth(rows) by 1 or nt)
0366 
0367 
0368           % Expand cols (time) of field involved
0369           if size(val,2) > size(storage.(tbl),2)
0370               storage.(tbl) = storage.(tbl) * ones(1, size(val,2));
0371           end
0372 
0373           % Expand rows (ess units) if change modifies submatrix of the parameter that is being modified (not all rows)
0374           if size(storage.(tbl), 1) == 1 && any(rows ~= 0)
0375               storage.(tbl) = ones(ns, 1) * storage.(tbl);
0376           end
0377 
0378           % Expand rows of val to match ess units to change
0379           if size(val,1) == 1 && size(storage.(tbl), 1) == ns && length(rows) > 1
0380               val = ones(length(rows), 1) * val;
0381           end
0382           if size(val,1) == 1 && size(storage.(tbl), 1) == ns && length(rows) == 1 && rows == 0
0383               val = ones(ns, 1) * val;
0384           end
0385 
0386           % Expands cols of val to match field's time dimension
0387           if size(val, 2) == 1 && size(storage.(tbl), 2) == nt_adhoc
0388               val = val * ones(1, nt_adhoc);
0389           end
0390 
0391           % Error if chgtype is CT_REL or CT_ADD and field involved is empty
0392           if (chgtyp == PR_REL || chgtyp == PR_ADD) && isempty(storage.(tbl))
0393               error('apply_profile: PR_REL or PR_ADD modification cannot be done to storage.%s if it is empty',tbl)
0394           end
0395 
0396 % (E.4) Apply change
0397           if length(rows) == 1 && rows == 0                 %% modify all rows
0398             if chgtyp == PR_REP                                    %% replace
0399               storage.(tbl) = val;
0400             elseif chgtyp == PR_REL                                %% scale
0401               storage.(tbl) = val .* storage.(tbl);
0402             elseif chgtyp == PR_ADD                                %% shift
0403               storage.(tbl) = val + storage.(tbl);
0404             else
0405               error('apply_profile: modification type %d for storage table not supported', chgtyp);
0406             end
0407           else                                              %% modify single row
0408             if chgtyp == PR_REP                                    %% replace
0409               storage.(tbl)(rows,:) = val;
0410             elseif chgtyp == PR_REL                                %% scale
0411               storage.(tbl)(rows,:) = val .* storage.(tbl)(rows,:);
0412             elseif chgtyp == PR_ADD                                %% shift
0413               storage.(tbl)(rows,:) = val + storage.(tbl)(rows,:);
0414             else
0415               error('apply_profile: modification type %d for storage table not supported', chgtyp);
0416             end
0417           end
0418         argout = storage;
0419 end

Generated on Fri 16-Dec-2016 12:45:37 by m2html © 2005