一种精细化计算设备状态的方法

在面板/半导体行业领域,设备状态的管理一般包含以下两个层面:

1,设备状态管理的层级:一般分为3个层级。主设备(EQP)、一级子单元(Unit),二级子单元(SubUnit)。

2,设备状态的定义:RUN(运行)、IDLE(空闲)、DOWN(宕机)、PM(保养)、ENG(调机)、TEST(测试)、JC(换线)等状态。

这两大方面是设备稼动率、OEE等指标计算的基础。行业内的各公司在这两大基础面的定义相差无几。然而,基于设备状态切换历史,进而计算设备的各项稼动指标的具体逻辑,则不尽然相同。

考虑多腔室类型的设备,借用编程的术语,这些设备具备“并发”能力。举例来说,某设备EQP_MAIN具有两个制程能力相同的UNIT,分别为UNIT1和UNIT2。若UNIT1为DOWN且UNIT2为RUN,此状态持续1小时,此时,无论EQP_MAIN挂RUN还是DOWN,均需要考虑:这1小时并非完整的DOWN或者RUN,而应该折算成RUN 0.5小时和DOWN 0.5小时。

现实世界中的设备层级要远复杂于此例子。但借鉴此思想,对各设备一级单元(Unit)或者二级子单元(SubUnit)进行合理划分,然后考虑不同UNIT状态的组合,并按其对设备状态影响程度,赋予不同的权重因子,则可实现对设备状态精细化的计算。

本文接下来主要描述如何设计算法和程序,来实现这种计算。

首先考虑以下几个要素:

计算时间点的选择:有两种选择,一是预先算好,例如采用ETL的方式。二是实时查询并计算。由于用户要求的应用场景等多种因素限制(例如PM等状态是人为标注的,且存在多次修改备注信息的场景,所以预先算好的方式不适用),本文采用了实时计算的方式。

查询性能的预估分析:由于设备的状态切换历史非常频繁,所以表中的数据量非常庞大。假如用户的查询范围是2个月内全部设备,则查询原始数据,然后再进行各种复杂计算,可以想象,要想快速计算出结果,必然是一个相当大的挑战。

系统架构的限制:不幸的是,面板行业/半导体行业的报表系统架构,基本上是采用集中式架构。此需求根本无法应用分布式计算框架。只能通过精心设计程序和算法,充分压榨oracle服务器和应用服务器,实现大数据量的快速计算。

计算资源的分配:报表采用的WEB架构,所以不可能在本机计算。只能是在DB服务器或者应用服务器上进行计算。以此需求为例,从DB试select 1台设备1个月的原始数据,一共4万笔,大约需要10多秒钟。select 1天,一共1千多笔,则需要300ms。考虑到底层数据是设备状态的原始数据,而计算过程相当复杂,如果把所有的逻辑计算放在DB服务器,对用户而言,查询的响应时间必然很慢。所以要合理平衡DB服务器和应用服务器,分配合适的计算任务。

进行以上要素分析和试验之后,采用以下设计:

1,采用分治法。在oracle端,将原始数据按设备按天切割(切割点为每天的08:00),然后发给应用服务器。应用服务器采用java/C#等编程语言,对按天切割后的数据,进行复杂逻辑运算。然后应用服务器对子计算任务的结果进行合并,作为最终结果。

2,虽然有多个子计算任务,但由于DB服务器和应用器同时并发,所以整体的查询响应时间,优于仅在oracle 上运算的方案。

3,状态组合的逻辑、各单元的权重等信息,设计配置表,由用户来维护。维护表需要包含以下信息:

  • 设备单元信息。定义了各个设备、各级子单元等信息。
  • 设备的状态关键单元。定义每个设备要查看哪些关键单元。
  • 设备的状态组合与状态权重的对应关系。定义关键单元的每种状态组合下,其状态权重。

接下来进行按天查询指定设备算法的设计。

1,应用服务器同时请求查询原始数据表和配置信息表。

private DataTable QueryRawData(DateTime periodDate, string machineName)
{
    var sql = $@"
                select a.machinename as {RawDataTable.MACHINE_NAME}, a.timekey as {RawDataTable.EVENT_TIME},
                        a.oldmachinestatename as {RawDataTable.PREV_STATUS}, a.machinestatename as {RawDataTable.CUR_STATUS}
                from CT_MACHINEHISTORY a
                where 1=1
                and a.machinename like '{machineName}%'
                and timekey>='{periodDate.ToString("yyyyMMddHH")}'
                and timekey<'{periodDate.AddDays(1).ToString("yyyyMMddHH")}'
                order by a.machinename, a.timekey
                ";

    var dtRaw = ConnectionKey.MESDBDG.Query(sql);
    return dtRaw;
}

private DataTable QueryEqpSpec(string machineName)
{
    var sql = $@"
                select a.machinename as {SpecDataTable.MACHINE_NAME}, 
                    a.detailmachinetype as detail_machine_type
                from MACHINESPEC a
                where 1=1
                and a.machinename like '{machineName}%'
                order by a.machinename";

    var dt = ConnectionKey.MESDBDG.Query(sql);
    return dt;
}

private DataTable QueryEqpStatusFactorSpec(string machineName)
{
    var sql = $@"
                select a.MACHINE_NAME as {EqpStatusFactorSpecTable.MACHINE_NAME}, 
                    a.KEY_UNIT_LIST as {EqpStatusFactorSpecTable.KEY_UNIT_LIST},
                    a.RATIO_LIST as {EqpStatusFactorSpecTable.RATIO_LIST},
                    REGEXP_REPLACE(REGEXP_REPLACE(a.UNIT_STATUS_PATTERN,'\*','.'),',','') as {EqpStatusFactorSpecTable.UNIT_STATUS_PATTERN},
                    a.DEFINED_MACHINE_STATUS as {EqpStatusFactorSpecTable.DEFINED_MACHINE_STATUS}
                from Eqp_Status_Factor_Spec a
                where 1=1
                and a.MACHINE_NAME = '{machineName}'";

    var dt = ConnectionKey.RPTDB.Query(sql);
    return dt;
}

注:配置表有2个。分别在不同的DB。原因是其中一个DB为readonly,不能建表。而已有的配置信息不够全,需要完善。

2,找出该天中“缺少”的单元,并补出它在当天的状态。由于每个设备包含多个单元,而有些单元可能在该天无任何状态变化,则这样原始数据表中无此单元的记录。但是状态计算时,需要考虑到每个单元,所以需要“补偿”。基本思想就是,通过配置表,找到完整的单元集合。然后减去有实际抓取出来的单元集合,得到“缺少”的单元集合。然后将“缺少”的每个单元,找出其离指定最近的前一笔记录,该笔记录的设备单元状态,作为该单元在指定天起点和终点的状态(全天维持状态不变)。

要说明的是,如果存在多个“缺少”的单元,假如采用每笔问询一次oracle,则会大大降低整体查询效能。所以,这里优化设计,仅查询1次。

private DataTable CompensateMissedHistory(DateTime periodDate, DataTable dtEqpSpec, DataTable dtRaw)
{
    var dt1 = dtEqpSpec.SelectDistinct(SpecDataTable.MACHINE_NAME);
    var dt2 = dtRaw.SelectDistinct(RawDataTable.MACHINE_NAME);
    var dt3 = dt1.Except(dt2);
    if (dt3.Rows.Count > 0)
    {
        var dt4 = QueryPrevRawData(periodDate, dt3.GetColumnData<string>(0));
        var dt5 = dt4.Clone();
        foreach (DataRow dr in dt4.Rows)
        {
            dt5.Rows.Add(dr[0], periodDate.ToString("yyyyMMddHHmmssffffff"), dr[3], dr[3]);
            dt5.Rows.Add(dr[0], periodDate.AddDays(1).ToString("yyyyMMddHHmmssffffff"), dr[3], dr[3]);
        }

        return dt5;
    }

    return dt3;
}

private DataTable QueryPrevRawData(DateTime periodDate, IEnumerable<string> machineNames)
{
    var sqlFormat = @"
                select * from
                (
                    select a.machinename as {0}, a.timekey as {1},
                        a.oldmachinestatename as prev_status, a.machinestatename as cur_status
                    from CT_MACHINEHISTORY a
                    where 1=1
                    and a.machinename = '{2}'
                    and timekey<'{3}'
                    and rownum<=1 
                    order by a.machinename, a.timekey desc
                )
                ";

    var sql = new StringBuilder();
    var uninVerb = " union ";
    foreach (var machineName in machineNames)
    {
        sql.AppendFormat(sqlFormat, RawDataTable.MACHINE_NAME, RawDataTable.EVENT_TIME, machineName, periodDate.ToString("yyyyMMddHH"));
        sql.Append(uninVerb);
    }

    sql = sql.Remove(sql.Length - uninVerb.Length, uninVerb.Length);

    var dt = ConnectionKey.MESDBDG.Query(sql);
    return dt;
}

3,将各单元按每天的时间切割点进行“对齐”。各单元在该天的首笔状态切换,不会刚好发生在切割点时间。所以需要补偿出从时间切割点到首笔状态变化之间的状态。同理,各单元在该天的末笔状态切换,也不会刚好发生在第二天的切割点时间。所以需要补偿出末笔到第二天切割点之间的状态。

private DataTable CompensateDateTimeBoundary(DateTime periodDate, DataTable dtRaw)
{
    var ds = dtRaw.GroupBy(RawDataTable.MACHINE_NAME);
    var startTime = periodDate.ToString("yyyyMMddHHmmssffffff");
    var endTime = periodDate.AddDays(1).ToString("yyyyMMddHHmmssffffff");

    for (var i = 1; i < ds.Tables.Count; i++)
    {
        var dt = ds.Tables[i];
        if (!object.Equals(dt.Rows[0][1], startTime))
        {
            var dr1 = dt.NewRow();
            dr1[0] = dt.Rows[0][0];
            dr1[1] = startTime;
            dr1[3] = dr1[2] = dt.Rows[0][2];
            dt.Rows.InsertAt(dr1, 0);
        }

        if (!object.Equals(dt.Rows[dt.Rows.Count - 1][0], endTime))
        {
            var dr2 = dt.NewRow();
            dr2[0] = dt.Rows[dt.Rows.Count - 1][0];
            dr2[1] = endTime;
            dr2[3] = dr2[2] = dt.Rows[dt.Rows.Count - 1][3];
            dt.Rows.Add(dr2);
        }
    }

    var dtResult = ds.Tables[1].Clone();
    for (var i = 1; i < ds.Tables.Count; i++)
    {
        dtResult = dtResult.UnionAll(ds.Tables[i]);
    }
    return dtResult;
}

4,至此,该天该设备的全部单元的切换历史,就准备完成了。不过进一步分析原始数据,发现有些单元会存在少量的这种情况:下一笔的状态和上一笔的状态相同。所以,为降低该天时间切割的区间数量,减少后续的计算量,可以对这种情况进行优化。也就说,对状态维持不变的多笔记录,"压缩“成1笔记录。

private DataTable CompressRawData(DataTable dtRaw)
{
    var dt = dtRaw.Clone();
    dt.ImportRow(dtRaw.Rows[0]);
    for (var i = 1; i < dtRaw.Rows.Count - 1; i++)
    {
        if (dtRaw.Rows[i][3].ToString() == dtRaw.Rows[i - 1][3].ToString() &&
            //dtRaw.Rows[i][2].ToString() == dtRaw.Rows[i - 1][2].ToString() &&
            dtRaw.Rows[i][0].ToString() == dtRaw.Rows[i - 1][0].ToString())
            continue;

        dt.ImportRow(dtRaw.Rows[i]);
    }
    return dt;
}

5,得到这些数据后,就可以在应用服务器进行开始进行逻辑运算。为根据指定的状态组合组合和权重进行计算,有考虑设计关联查询的方案和类似掩码的方案。关联查询方案的方案理论可行,但是当设备的单元够多时(例如有N个单元),则各单元的可能的状态组合数是7的N次方。计算量非常庞大,实际不可行。采用类似掩码的方案,一番分析下来发现很难实现。首先是每个单元状态有7种,0和1的方案本身就无法代表。如果要扩展,则要判断状态时,会变得非常复杂,基本不可行。综合先来,唯一可行的解法,就是设计正则表达式。这样实现了以尽可能少的行覆盖了全部的状态组合。既方便用户建配置表,又能使程序快速得到权重规则,进而基于规则再进行计算。

考虑到为提高运算效率,分析7种状态,发现首字母各不相同,故分别用首字母代表各个状态。算法简述如下:

  • 首先,将设备单元的实际状态,提取首字母,“浓缩”成一个字符串。这个字符串代表当前设备各个单元的状态组合。
  • 接下来,以上述字符串作为正则表达式的输入,扫描配置表中用户定义的全部状态组合模式,进行正则表达式模式匹配。一旦匹配成功,取出该状态对应的权重计算规则。为方便用户建立和维护配置表,状态组合模式并非全部采用正则表达式语法,故需要在取配置表时做一定的替换。例如,用户建表时,让其输入‘*’代表任意字符,并将各状态以','分隔方便查看。取出来时要转成‘.’,且为了提高匹配效率,去掉了','。
  • 然后,分析权重计算规则,将设备实际的单元状态,回填到规则,生成各个状态的权重系数。权重规则中,是要捕获实际的状态值。这里有两种设计法。一种是按正则表达式的组捕获方法,来设计规则输入模式;另一种,采用字符串占位符,然后编写解析和回填方法。为提高程序运行效率且便于用户理解,这里采用了第二种设计。
  • 再下来,对已生成的各状态权重系数进行同类求和,得到最终结果。
public DataTable BuildResult(DateTime periodDate, string machineName)
{
    var dtRaw = QueryRawData(periodDate, machineName);
    var dtSpec = QueryEqpSpec(machineName);
    var dtFactorSpec = QueryEqpStatusFactorSpec(machineName);

    var dtMissed = CompensateMissedHistory(periodDate, dtSpec, dtRaw);
    var dtRawCompensated = CompensateDateTimeBoundary(periodDate, dtRaw);
    var dtRawComplete = dtRawCompensated.UnionAll(dtMissed);
    var dtRawCompressed = CompressRawData(dtRawComplete);

    var dtDateTimePoints = dtRawComplete.SelectDistinct(RawDataTable.EVENT_TIME).OrderByAsc(RawDataTable.EVENT_TIME);
    var dtResult = dtDateTimePoints.Copy();

    var ds = dtRawCompressed.GroupBy(RawDataTable.MACHINE_NAME);
    for (var i = 1; i < ds.Tables.Count; i++)
    {
        var dt = ds.Tables[i].SelectFrom(RawDataTable.EVENT_TIME, RawDataTable.CUR_STATUS).
            RenameColumns(RawDataTable.CUR_STATUS, ds.Tables[0].Rows[i - 1][0].ToString());
        dtResult = dtResult.LeftJoin(dt, RawDataTable.EVENT_TIME);
    }

    for (var i = 1; i < dtResult.Rows.Count; i++)
    {
        for (var j = 1; j < dtResult.Columns.Count; j++)
        {
            if (dtResult.Rows[i][j] == DBNull.Value)
            {
                dtResult.Rows[i][j] = dtResult.Rows[i - 1][j];
                dtResult.Rows[i][j] = dtResult.Rows[i - 1][j];
            }
        }
    }

    dtResult = Compute(dtResult, dtFactorSpec);
    return dtResult;
}

上述代码主要是完成原始数据的准备。包含为了方便计算,进行的空值补偿。

核心的计算逻辑如下:

public DataTable Compute(DataTable dtResult, DataTable dtFactorSpec)
{
    var dc = dtResult.AddColumn<string>("CombinedUnitStatus");
    var allStatus = new string[] { "E", "P", "T", "J", "D", "I", "R" };
    dtResult.AddColumns<decimal>(allStatus);

    var keyUnitList = dtFactorSpec.Rows[0][EqpStatusFactorSpecTable.KEY_UNIT_LIST].ToString().Split(',').Select(x => x.Trim()).ToArray();
    dc.Expression = keyUnitList.Select(x => $"SUBSTRING([{x}],1,1)").ToText('+');

    var patternList = dtFactorSpec.SelectFrom(EqpStatusFactorSpecTable.UNIT_STATUS_PATTERN).GetColumnData<string>(0).ToArray();
    var definedMachineStatusList = dtFactorSpec.SelectFrom(EqpStatusFactorSpecTable.DEFINED_MACHINE_STATUS).GetColumnData<string>(0).Select(x => x.Trim()).ToArray();

    for (var i = 0; i < dtResult.Rows.Count; i++)
    {
        var dr = dtResult.Rows[i];
        var combinedStatus = dr["CombinedUnitStatus"].ToString();
        for (var j = 0; j < patternList.Length; j++)
        {
            if (!Regex.IsMatch(combinedStatus, patternList[j]))
                continue;

            var definedStatus = definedMachineStatusList[j];
            var actualStatus = ReplaceWithActual(combinedStatus, definedStatus);
            var dictWeight = SumStatusWeight(actualStatus);

            foreach (var key in dictWeight.Keys)
            {
                dr[key] = dictWeight[key];
            }

            break;
        }
    }

    return dtResult;
}

其中,根据实际值回填规则,生成各个状态的权重系数的方法如下:

private string ReplaceWithActual(string combinedStatus, string definedMachineStatus)
{
    if (!definedMachineStatus.Contains("{"))
        return definedMachineStatus;

    var patterns = definedMachineStatus.Split(',');
    var actualList = new string[patterns.Length];
    for (var i = 0; i < patterns.Length; i++)
    {
        if (!patterns[i].StartsWith("{"))
            continue;

        var index = int.Parse(patterns[i].Substring(1, patterns[i].IndexOf('}') - 1));
        actualList[i] = $@"{combinedStatus[index]}:{patterns[i].Substring(patterns[i].IndexOf(':') + 1)}";
    }

    return actualList.ToText(',');
}

对同类权重系数求和的方法如下:

private Dictionary<string, decimal> SumStatusWeight(string acutalStatus)
{
    var dict = new Dictionary<string, decimal>();
    var actualStatusArray = acutalStatus.Split(',');
    foreach (var status in actualStatusArray)
    {
        var kv = status.Split(':');
        if (!dict.ContainsKey(kv[0]))
            dict.Add(kv[0], Fraction.FromString(kv[1]).ToDecimal());
        else
            dict[kv[0]] += Fraction.FromString(kv[1]).ToDecimal();
    }

    return dict;
}

至此,计算出某个设备在指定天的精细化的状态。后续的展示及更高一层的统计,便可以此为基础数据。

如果要计算多个设备在多天的精细化状态,便可采用多并行编程的方式,进行并行计算。这里不再赘述。

另外要说明的一点,为方便高效开发,程序设计中并未采用linq to dataset的方式进行数据查询和处理,而是自己开发出一套完整的DataTable Extension 流式API,对DataTable进行类似ORACLE式的操作,使得开发更高效,代码更简洁。例如代码中所示的LeftJoin, GroupBy等方法。

完整的程序截图如下。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值